@trazum/core 1.8.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 +21 -0
- package/README.md +163 -0
- package/dist/advisories.d.ts +23 -0
- package/dist/advisories.d.ts.map +1 -0
- package/dist/advisories.js +376 -0
- package/dist/advisories.js.map +1 -0
- package/dist/aws-sigv4.d.ts +88 -0
- package/dist/aws-sigv4.d.ts.map +1 -0
- package/dist/aws-sigv4.js +117 -0
- package/dist/aws-sigv4.js.map +1 -0
- package/dist/baseline.d.ts +171 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +273 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cache.d.ts +26 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +28 -0
- package/dist/cache.js.map +1 -0
- package/dist/changes.d.ts +29 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +142 -0
- package/dist/changes.js.map +1 -0
- package/dist/compare.d.ts +65 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +58 -0
- package/dist/compare.js.map +1 -0
- package/dist/config-schema.d.ts +118 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config-schema.js +315 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +112 -0
- package/dist/config.js.map +1 -0
- package/dist/detect.d.ts +70 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +228 -0
- package/dist/detect.js.map +1 -0
- package/dist/evaluate.d.ts +98 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +110 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/extract.d.ts +81 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +280 -0
- package/dist/extract.js.map +1 -0
- package/dist/gcp-auth.d.ts +58 -0
- package/dist/gcp-auth.d.ts.map +1 -0
- package/dist/gcp-auth.js +113 -0
- package/dist/gcp-auth.js.map +1 -0
- package/dist/glob.d.ts +49 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +154 -0
- package/dist/glob.js.map +1 -0
- package/dist/host.d.ts +30 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +69 -0
- package/dist/host.js.map +1 -0
- package/dist/i18n/en.d.ts +4 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +168 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/es.d.ts +4 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +168 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/index.d.ts +36 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +50 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/types.d.ts +180 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +11 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/llm.d.ts +226 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +485 -0
- package/dist/llm.js.map +1 -0
- package/dist/nearest.d.ts +20 -0
- package/dist/nearest.d.ts.map +1 -0
- package/dist/nearest.js +54 -0
- package/dist/nearest.js.map +1 -0
- package/dist/net.d.ts +90 -0
- package/dist/net.d.ts.map +1 -0
- package/dist/net.js +203 -0
- package/dist/net.js.map +1 -0
- package/dist/node.d.ts +32 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +33 -0
- package/dist/node.js.map +1 -0
- package/dist/openrouter.d.ts +25 -0
- package/dist/openrouter.d.ts.map +1 -0
- package/dist/openrouter.js +72 -0
- package/dist/openrouter.js.map +1 -0
- package/dist/optimize.d.ts +38 -0
- package/dist/optimize.d.ts.map +1 -0
- package/dist/optimize.js +183 -0
- package/dist/optimize.js.map +1 -0
- package/dist/otlp.d.ts +91 -0
- package/dist/otlp.d.ts.map +1 -0
- package/dist/otlp.js +102 -0
- package/dist/otlp.js.map +1 -0
- package/dist/phrases.d.ts +169 -0
- package/dist/phrases.d.ts.map +1 -0
- package/dist/phrases.js +939 -0
- package/dist/phrases.js.map +1 -0
- package/dist/pricing-overlay.d.ts +55 -0
- package/dist/pricing-overlay.d.ts.map +1 -0
- package/dist/pricing-overlay.js +241 -0
- package/dist/pricing-overlay.js.map +1 -0
- package/dist/pricing.d.ts +115 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +400 -0
- package/dist/pricing.js.map +1 -0
- package/dist/profile.d.ts +71 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +55 -0
- package/dist/profile.js.map +1 -0
- package/dist/promptfoo.d.ts +58 -0
- package/dist/promptfoo.d.ts.map +1 -0
- package/dist/promptfoo.js +149 -0
- package/dist/promptfoo.js.map +1 -0
- package/dist/prune.d.ts +91 -0
- package/dist/prune.d.ts.map +1 -0
- package/dist/prune.js +110 -0
- package/dist/prune.js.map +1 -0
- package/dist/reorder.d.ts +82 -0
- package/dist/reorder.d.ts.map +1 -0
- package/dist/reorder.js +215 -0
- package/dist/reorder.js.map +1 -0
- package/dist/review.d.ts +54 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +131 -0
- package/dist/review.js.map +1 -0
- package/dist/rules.d.ts +5 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +279 -0
- package/dist/rules.js.map +1 -0
- package/dist/savings.d.ts +36 -0
- package/dist/savings.d.ts.map +1 -0
- package/dist/savings.js +83 -0
- package/dist/savings.js.map +1 -0
- package/dist/segment.d.ts +8 -0
- package/dist/segment.d.ts.map +1 -0
- package/dist/segment.js +74 -0
- package/dist/segment.js.map +1 -0
- package/dist/shared-prefix.d.ts +63 -0
- package/dist/shared-prefix.d.ts.map +1 -0
- package/dist/shared-prefix.js +151 -0
- package/dist/shared-prefix.js.map +1 -0
- package/dist/similarity.d.ts +13 -0
- package/dist/similarity.d.ts.map +1 -0
- package/dist/similarity.js +30 -0
- package/dist/similarity.js.map +1 -0
- package/dist/structure.d.ts +144 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +455 -0
- package/dist/structure.js.map +1 -0
- package/dist/suggest.d.ts +100 -0
- package/dist/suggest.d.ts.map +1 -0
- package/dist/suggest.js +151 -0
- package/dist/suggest.js.map +1 -0
- package/dist/tokenizer.d.ts +57 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +157 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/walk.d.ts +40 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +95 -0
- package/dist/walk.js.map +1 -0
- package/package.json +56 -0
- package/src/advisories.ts +431 -0
- package/src/aws-sigv4.ts +174 -0
- package/src/baseline.ts +390 -0
- package/src/cache.ts +54 -0
- package/src/changes.ts +158 -0
- package/src/compare.ts +131 -0
- package/src/config-schema.ts +451 -0
- package/src/config.ts +161 -0
- package/src/detect.ts +312 -0
- package/src/evaluate.ts +188 -0
- package/src/extract.ts +336 -0
- package/src/gcp-auth.ts +166 -0
- package/src/glob.ts +160 -0
- package/src/host.ts +90 -0
- package/src/i18n/en.ts +236 -0
- package/src/i18n/es.ts +236 -0
- package/src/i18n/index.ts +68 -0
- package/src/i18n/types.ts +230 -0
- package/src/index.ts +228 -0
- package/src/llm.ts +708 -0
- package/src/nearest.ts +61 -0
- package/src/net.ts +233 -0
- package/src/node.ts +63 -0
- package/src/openrouter.ts +125 -0
- package/src/optimize.ts +228 -0
- package/src/otlp.ts +179 -0
- package/src/phrases.ts +1047 -0
- package/src/pricing-overlay.ts +319 -0
- package/src/pricing.ts +468 -0
- package/src/profile.ts +124 -0
- package/src/promptfoo.ts +213 -0
- package/src/prune.ts +211 -0
- package/src/reorder.ts +307 -0
- package/src/review.ts +180 -0
- package/src/rules.ts +324 -0
- package/src/savings.ts +121 -0
- package/src/segment.ts +106 -0
- package/src/shared-prefix.ts +198 -0
- package/src/similarity.ts +28 -0
- package/src/structure.ts +652 -0
- package/src/suggest.ts +254 -0
- package/src/tokenizer.ts +190 -0
- package/src/types.ts +323 -0
- package/src/walk.ts +117 -0
package/src/pricing.ts
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import type { ModelPricing } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Model and pricing catalogue (USD per million tokens).
|
|
5
|
+
*
|
|
6
|
+
* Source: official Claude API documentation. Prices change: check
|
|
7
|
+
* `PRICING_LAST_REVIEWED` and update this file before making budget decisions.
|
|
8
|
+
* Amazon Bedrock and Vertex AI pricing is set by each partner and is NOT the
|
|
9
|
+
* pricing in this table.
|
|
10
|
+
*/
|
|
11
|
+
export const PRICING_LAST_REVIEWED = '2026-06-24';
|
|
12
|
+
|
|
13
|
+
/** Cost multipliers relative to the input price. */
|
|
14
|
+
/**
|
|
15
|
+
* The defaults, which are Anthropic's numbers.
|
|
16
|
+
*
|
|
17
|
+
* Still exported and still correct for every Anthropic model, but no longer the
|
|
18
|
+
* whole story: a model can override any of these through `multipliers`, and
|
|
19
|
+
* anything computing a cost should go through `multipliersFor` rather than
|
|
20
|
+
* reading these directly. A cache read is ~10% of input on Anthropic and ~50%
|
|
21
|
+
* on OpenAI, and using one number for both invents a saving.
|
|
22
|
+
*/
|
|
23
|
+
export const COST_MULTIPLIERS = {
|
|
24
|
+
/** Cache write with a 5-minute TTL. */
|
|
25
|
+
cacheWrite5m: 1.25,
|
|
26
|
+
/** Cache write with a 1-hour TTL. */
|
|
27
|
+
cacheWrite1h: 2.0,
|
|
28
|
+
/** Cache read: ~10% of the input price. */
|
|
29
|
+
cacheRead: 0.1,
|
|
30
|
+
/** Batch API: 50% discount on input and output. */
|
|
31
|
+
batch: 0.5,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The multipliers that apply to one model, defaults filled in.
|
|
36
|
+
*
|
|
37
|
+
* `batch` stays `null` when the provider has no batch API, which is different
|
|
38
|
+
* from "unspecified": the first should stop the advisory firing, the second
|
|
39
|
+
* should fall back to the default.
|
|
40
|
+
*/
|
|
41
|
+
export function multipliersFor(model: ModelPricing): {
|
|
42
|
+
cacheWrite5m: number;
|
|
43
|
+
cacheWrite1h: number;
|
|
44
|
+
cacheRead: number;
|
|
45
|
+
batch: number | null;
|
|
46
|
+
} {
|
|
47
|
+
const m = model.multipliers ?? {};
|
|
48
|
+
return {
|
|
49
|
+
cacheWrite5m: m.cacheWrite5m ?? COST_MULTIPLIERS.cacheWrite5m,
|
|
50
|
+
cacheWrite1h: m.cacheWrite1h ?? COST_MULTIPLIERS.cacheWrite1h,
|
|
51
|
+
cacheRead: m.cacheRead ?? COST_MULTIPLIERS.cacheRead,
|
|
52
|
+
batch: m.batch === undefined ? COST_MULTIPLIERS.batch : m.batch,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const MODELS: ModelPricing[] = [
|
|
57
|
+
{
|
|
58
|
+
id: 'claude-fable-5',
|
|
59
|
+
provider: 'anthropic',
|
|
60
|
+
displayName: 'Claude Fable 5',
|
|
61
|
+
inputPerMTok: 10,
|
|
62
|
+
outputPerMTok: 50,
|
|
63
|
+
contextWindow: 1_000_000,
|
|
64
|
+
cacheMinTokens: 512,
|
|
65
|
+
capability: 'frontier',
|
|
66
|
+
tier: 'frontier',
|
|
67
|
+
notes: 'Highest capability. Requires 30-day data retention.',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'claude-mythos-5',
|
|
71
|
+
provider: 'anthropic',
|
|
72
|
+
displayName: 'Claude Mythos 5',
|
|
73
|
+
recommendable: false,
|
|
74
|
+
inputPerMTok: 10,
|
|
75
|
+
outputPerMTok: 50,
|
|
76
|
+
contextWindow: 1_000_000,
|
|
77
|
+
cacheMinTokens: 512,
|
|
78
|
+
capability: 'frontier',
|
|
79
|
+
tier: 'frontier',
|
|
80
|
+
notes: 'Available only through Project Glasswing.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'claude-opus-5',
|
|
84
|
+
provider: 'anthropic',
|
|
85
|
+
displayName: 'Claude Opus 5',
|
|
86
|
+
inputPerMTok: 5,
|
|
87
|
+
outputPerMTok: 25,
|
|
88
|
+
contextWindow: 1_000_000,
|
|
89
|
+
cacheMinTokens: 512,
|
|
90
|
+
capability: 'large',
|
|
91
|
+
tier: 'opus',
|
|
92
|
+
notes: '512-token cache minimum: caches prompts that would miss on Opus 4.6.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'claude-opus-4-8',
|
|
96
|
+
provider: 'anthropic',
|
|
97
|
+
displayName: 'Claude Opus 4.8',
|
|
98
|
+
inputPerMTok: 5,
|
|
99
|
+
outputPerMTok: 25,
|
|
100
|
+
contextWindow: 1_000_000,
|
|
101
|
+
cacheMinTokens: 1024,
|
|
102
|
+
capability: 'large',
|
|
103
|
+
tier: 'opus',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'claude-opus-4-7',
|
|
107
|
+
provider: 'anthropic',
|
|
108
|
+
displayName: 'Claude Opus 4.7',
|
|
109
|
+
inputPerMTok: 5,
|
|
110
|
+
outputPerMTok: 25,
|
|
111
|
+
contextWindow: 1_000_000,
|
|
112
|
+
cacheMinTokens: 2048,
|
|
113
|
+
capability: 'large',
|
|
114
|
+
tier: 'opus',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'claude-opus-4-6',
|
|
118
|
+
provider: 'anthropic',
|
|
119
|
+
displayName: 'Claude Opus 4.6',
|
|
120
|
+
inputPerMTok: 5,
|
|
121
|
+
outputPerMTok: 25,
|
|
122
|
+
contextWindow: 1_000_000,
|
|
123
|
+
cacheMinTokens: 4096,
|
|
124
|
+
capability: 'large',
|
|
125
|
+
tier: 'opus',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'claude-sonnet-5',
|
|
129
|
+
provider: 'anthropic',
|
|
130
|
+
displayName: 'Claude Sonnet 5',
|
|
131
|
+
inputPerMTok: 3,
|
|
132
|
+
outputPerMTok: 15,
|
|
133
|
+
contextWindow: 1_000_000,
|
|
134
|
+
cacheMinTokens: 1024,
|
|
135
|
+
capability: 'mid',
|
|
136
|
+
tier: 'sonnet',
|
|
137
|
+
promo: { inputPerMTok: 2, outputPerMTok: 10, until: '2026-08-31' },
|
|
138
|
+
notes: 'Introductory pricing of 2/10 until 2026-08-31; 3/15 afterwards.',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'claude-sonnet-4-6',
|
|
142
|
+
provider: 'anthropic',
|
|
143
|
+
displayName: 'Claude Sonnet 4.6',
|
|
144
|
+
inputPerMTok: 3,
|
|
145
|
+
outputPerMTok: 15,
|
|
146
|
+
contextWindow: 1_000_000,
|
|
147
|
+
cacheMinTokens: 1024,
|
|
148
|
+
capability: 'mid',
|
|
149
|
+
tier: 'sonnet',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'claude-haiku-4-5',
|
|
153
|
+
provider: 'anthropic',
|
|
154
|
+
displayName: 'Claude Haiku 4.5',
|
|
155
|
+
inputPerMTok: 1,
|
|
156
|
+
outputPerMTok: 5,
|
|
157
|
+
contextWindow: 200_000,
|
|
158
|
+
cacheMinTokens: 4096,
|
|
159
|
+
capability: 'small',
|
|
160
|
+
tier: 'haiku',
|
|
161
|
+
notes: '200K context window and a 64K output cap, smaller than the rest.',
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// ------------------------------------------------------------------------
|
|
165
|
+
// OpenAI
|
|
166
|
+
//
|
|
167
|
+
// Caching is AUTOMATIC above 1,024 tokens — there is no cache_control to set,
|
|
168
|
+
// and a cached read costs about half the input price rather than a tenth. The
|
|
169
|
+
// reordering advice is identical (a prefix is a prefix), the marker advice is
|
|
170
|
+
// not, which is why `caching` and `multipliers` exist at all.
|
|
171
|
+
// ------------------------------------------------------------------------
|
|
172
|
+
{
|
|
173
|
+
id: 'gpt-5',
|
|
174
|
+
provider: 'openai',
|
|
175
|
+
displayName: 'GPT-5',
|
|
176
|
+
inputPerMTok: 1.25,
|
|
177
|
+
outputPerMTok: 10,
|
|
178
|
+
contextWindow: 400_000,
|
|
179
|
+
cacheMinTokens: 1024,
|
|
180
|
+
caching: 'automatic',
|
|
181
|
+
multipliers: { cacheRead: 0.1, cacheWrite5m: 1, cacheWrite1h: 1, batch: 0.5 },
|
|
182
|
+
capability: 'frontier',
|
|
183
|
+
tier: 'frontier',
|
|
184
|
+
notes: 'Caching is automatic above 1,024 tokens; there is no cache_control to set.',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: 'gpt-5-mini',
|
|
188
|
+
provider: 'openai',
|
|
189
|
+
displayName: 'GPT-5 mini',
|
|
190
|
+
inputPerMTok: 0.25,
|
|
191
|
+
outputPerMTok: 2,
|
|
192
|
+
contextWindow: 400_000,
|
|
193
|
+
cacheMinTokens: 1024,
|
|
194
|
+
caching: 'automatic',
|
|
195
|
+
multipliers: { cacheRead: 0.1, cacheWrite5m: 1, cacheWrite1h: 1, batch: 0.5 },
|
|
196
|
+
capability: 'mid',
|
|
197
|
+
tier: 'sonnet',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'gpt-5-nano',
|
|
201
|
+
provider: 'openai',
|
|
202
|
+
displayName: 'GPT-5 nano',
|
|
203
|
+
inputPerMTok: 0.05,
|
|
204
|
+
outputPerMTok: 0.4,
|
|
205
|
+
contextWindow: 400_000,
|
|
206
|
+
cacheMinTokens: 1024,
|
|
207
|
+
caching: 'automatic',
|
|
208
|
+
multipliers: { cacheRead: 0.1, cacheWrite5m: 1, cacheWrite1h: 1, batch: 0.5 },
|
|
209
|
+
capability: 'small',
|
|
210
|
+
tier: 'haiku',
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
// ------------------------------------------------------------------------
|
|
214
|
+
// Google
|
|
215
|
+
// ------------------------------------------------------------------------
|
|
216
|
+
{
|
|
217
|
+
id: 'gemini-2.5-pro',
|
|
218
|
+
provider: 'google',
|
|
219
|
+
displayName: 'Gemini 2.5 Pro',
|
|
220
|
+
inputPerMTok: 1.25,
|
|
221
|
+
outputPerMTok: 10,
|
|
222
|
+
contextWindow: 1_048_576,
|
|
223
|
+
cacheMinTokens: 2048,
|
|
224
|
+
caching: 'explicit',
|
|
225
|
+
multipliers: { cacheRead: 0.25, cacheWrite5m: 1, cacheWrite1h: 1, batch: 0.5 },
|
|
226
|
+
capability: 'large',
|
|
227
|
+
tier: 'opus',
|
|
228
|
+
notes: 'Context caching is billed for storage per hour as well as per read.',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'gemini-2.5-flash',
|
|
232
|
+
provider: 'google',
|
|
233
|
+
displayName: 'Gemini 2.5 Flash',
|
|
234
|
+
inputPerMTok: 0.3,
|
|
235
|
+
outputPerMTok: 2.5,
|
|
236
|
+
contextWindow: 1_048_576,
|
|
237
|
+
cacheMinTokens: 1024,
|
|
238
|
+
caching: 'explicit',
|
|
239
|
+
multipliers: { cacheRead: 0.25, cacheWrite5m: 1, cacheWrite1h: 1, batch: 0.5 },
|
|
240
|
+
capability: 'mid',
|
|
241
|
+
tier: 'sonnet',
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
// ------------------------------------------------------------------------
|
|
245
|
+
// Moonshot
|
|
246
|
+
// ------------------------------------------------------------------------
|
|
247
|
+
{
|
|
248
|
+
id: 'kimi-k2',
|
|
249
|
+
provider: 'moonshot',
|
|
250
|
+
displayName: 'Kimi K2',
|
|
251
|
+
inputPerMTok: 0.6,
|
|
252
|
+
outputPerMTok: 2.5,
|
|
253
|
+
contextWindow: 256_000,
|
|
254
|
+
cacheMinTokens: 1024,
|
|
255
|
+
caching: 'automatic',
|
|
256
|
+
multipliers: { cacheRead: 0.1, cacheWrite5m: 1, cacheWrite1h: 1, batch: null },
|
|
257
|
+
capability: 'mid',
|
|
258
|
+
tier: 'sonnet',
|
|
259
|
+
notes: 'No batch API: the batch advisory stays quiet rather than offering a discount you cannot buy.',
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// ------------------------------------------------------------------------
|
|
263
|
+
// DeepSeek
|
|
264
|
+
// ------------------------------------------------------------------------
|
|
265
|
+
{
|
|
266
|
+
id: 'deepseek-v3',
|
|
267
|
+
provider: 'deepseek',
|
|
268
|
+
displayName: 'DeepSeek V3',
|
|
269
|
+
inputPerMTok: 0.27,
|
|
270
|
+
outputPerMTok: 1.1,
|
|
271
|
+
contextWindow: 128_000,
|
|
272
|
+
cacheMinTokens: 1024,
|
|
273
|
+
caching: 'automatic',
|
|
274
|
+
multipliers: { cacheRead: 0.1, cacheWrite5m: 1, cacheWrite1h: 1, batch: null },
|
|
275
|
+
capability: 'mid',
|
|
276
|
+
tier: 'sonnet',
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// ------------------------------------------------------------------------
|
|
280
|
+
// xAI
|
|
281
|
+
// ------------------------------------------------------------------------
|
|
282
|
+
{
|
|
283
|
+
id: 'grok-4',
|
|
284
|
+
provider: 'xai',
|
|
285
|
+
displayName: 'Grok 4',
|
|
286
|
+
inputPerMTok: 3,
|
|
287
|
+
outputPerMTok: 15,
|
|
288
|
+
contextWindow: 256_000,
|
|
289
|
+
cacheMinTokens: 1024,
|
|
290
|
+
caching: 'automatic',
|
|
291
|
+
multipliers: { cacheRead: 0.25, cacheWrite5m: 1, cacheWrite1h: 1, batch: null },
|
|
292
|
+
capability: 'large',
|
|
293
|
+
tier: 'opus',
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
// ------------------------------------------------------------------------
|
|
297
|
+
// Mistral
|
|
298
|
+
// ------------------------------------------------------------------------
|
|
299
|
+
{
|
|
300
|
+
id: 'mistral-large-2',
|
|
301
|
+
provider: 'mistral',
|
|
302
|
+
displayName: 'Mistral Large 2',
|
|
303
|
+
inputPerMTok: 2,
|
|
304
|
+
outputPerMTok: 6,
|
|
305
|
+
contextWindow: 128_000,
|
|
306
|
+
cacheMinTokens: 0,
|
|
307
|
+
caching: 'none',
|
|
308
|
+
multipliers: { batch: 0.5 },
|
|
309
|
+
capability: 'large',
|
|
310
|
+
tier: 'opus',
|
|
311
|
+
notes: 'No prompt caching: reordering still helps readability but saves nothing here.',
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
export const DEFAULT_MODEL = 'claude-opus-5';
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* A set of prices to work from.
|
|
319
|
+
*
|
|
320
|
+
* Prices change on someone else's schedule, and until 1.0 correcting one meant
|
|
321
|
+
* upgrading the library — which is backwards: a stale price is a wrong number in
|
|
322
|
+
* a budget decision, and nobody should have to take a dependency bump to fix it.
|
|
323
|
+
*
|
|
324
|
+
* So the catalogue is a **value**, not module state. The bundled one below is the
|
|
325
|
+
* default, and `applyPricingOverlay` returns a *new* catalogue with local
|
|
326
|
+
* corrections layered on. Nothing mutates: a caller who overlays prices does not
|
|
327
|
+
* change what any other caller sees, and two catalogues can exist in one process
|
|
328
|
+
* — which is what makes it testable and what stops one consumer's local prices
|
|
329
|
+
* leaking into another's report.
|
|
330
|
+
*/
|
|
331
|
+
export interface PricingCatalogue {
|
|
332
|
+
models: ModelPricing[];
|
|
333
|
+
byId: Map<string, ModelPricing>;
|
|
334
|
+
/** The date the prices in this catalogue were last checked. */
|
|
335
|
+
lastReviewed: string;
|
|
336
|
+
/** Ids whose bundled prices an overlay replaced. Empty for the bundled set. */
|
|
337
|
+
overriddenModels: string[];
|
|
338
|
+
/** Ids an overlay introduced that the bundled catalogue does not have. */
|
|
339
|
+
addedModels: string[];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function makeCatalogue(
|
|
343
|
+
models: ModelPricing[],
|
|
344
|
+
lastReviewed: string,
|
|
345
|
+
overriddenModels: string[] = [],
|
|
346
|
+
addedModels: string[] = [],
|
|
347
|
+
): PricingCatalogue {
|
|
348
|
+
return {
|
|
349
|
+
models,
|
|
350
|
+
byId: new Map(models.map((m) => [m.id, m])),
|
|
351
|
+
lastReviewed,
|
|
352
|
+
overriddenModels,
|
|
353
|
+
addedModels,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** The prices compiled into this release. */
|
|
358
|
+
export const BUNDLED_CATALOGUE: PricingCatalogue = makeCatalogue(MODELS, PRICING_LAST_REVIEWED);
|
|
359
|
+
|
|
360
|
+
/** Looks a model up in a specific catalogue. */
|
|
361
|
+
export function modelFrom(catalogue: PricingCatalogue, id: string): ModelPricing {
|
|
362
|
+
const model = catalogue.byId.get(id);
|
|
363
|
+
if (!model) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Unknown model: "${id}". Available: ${catalogue.models.map((m) => m.id).join(', ')}`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return model;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Cheapest model of a tier within a catalogue.
|
|
373
|
+
*
|
|
374
|
+
* Compared on the **effective** price, so a model in a promotional window is
|
|
375
|
+
* ranked at what it actually costs today rather than at its list price.
|
|
376
|
+
*/
|
|
377
|
+
export function cheapestOfTierIn(
|
|
378
|
+
catalogue: PricingCatalogue,
|
|
379
|
+
tier: ModelPricing['tier'],
|
|
380
|
+
on: Date = new Date(),
|
|
381
|
+
/**
|
|
382
|
+
* Restrict to one provider. Omit to search the whole catalogue, which is what
|
|
383
|
+
* this did before other providers existed — kept as the default so the
|
|
384
|
+
* signature stays additive, though the advisory always passes one: switching
|
|
385
|
+
* vendor is a migration, not a cheaper model.
|
|
386
|
+
*/
|
|
387
|
+
provider?: string,
|
|
388
|
+
): ModelPricing {
|
|
389
|
+
const candidates = catalogue.models.filter(
|
|
390
|
+
(m) =>
|
|
391
|
+
m.tier === tier &&
|
|
392
|
+
m.recommendable !== false &&
|
|
393
|
+
(provider === undefined || m.provider === provider),
|
|
394
|
+
);
|
|
395
|
+
const first = candidates[0];
|
|
396
|
+
if (!first) throw new Error(`No models for tier "${tier}"`);
|
|
397
|
+
return candidates.reduce(
|
|
398
|
+
(best, m) =>
|
|
399
|
+
effectivePricing(m, on).inputPerMTok < effectivePricing(best, on).inputPerMTok ? m : best,
|
|
400
|
+
first,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function getModel(id: string): ModelPricing {
|
|
405
|
+
return modelFrom(BUNDLED_CATALOGUE, id);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function listModels(): ModelPricing[] {
|
|
409
|
+
return [...MODELS];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Effective price on a given date, applying any live promotion. */
|
|
413
|
+
export function effectivePricing(
|
|
414
|
+
model: ModelPricing,
|
|
415
|
+
on: Date = new Date(),
|
|
416
|
+
): { inputPerMTok: number; outputPerMTok: number; promoApplied: boolean } {
|
|
417
|
+
if (model.promo) {
|
|
418
|
+
const until = new Date(`${model.promo.until}T23:59:59.999Z`);
|
|
419
|
+
if (on.getTime() <= until.getTime()) {
|
|
420
|
+
return {
|
|
421
|
+
inputPerMTok: model.promo.inputPerMTok,
|
|
422
|
+
outputPerMTok: model.promo.outputPerMTok,
|
|
423
|
+
promoApplied: true,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
inputPerMTok: model.inputPerMTok,
|
|
429
|
+
outputPerMTok: model.outputPerMTok,
|
|
430
|
+
promoApplied: false,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Cheapest model of each capability tier, for recommendations. */
|
|
435
|
+
export function cheapestOfTier(tier: ModelPricing['tier']): ModelPricing {
|
|
436
|
+
const candidates = MODELS.filter((m) => m.tier === tier && m.recommendable !== false);
|
|
437
|
+
const first = candidates[0];
|
|
438
|
+
if (!first) throw new Error(`No models for tier "${tier}"`);
|
|
439
|
+
return candidates.reduce((best, m) => (m.inputPerMTok < best.inputPerMTok ? m : best), first);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Whole days between a `YYYY-MM-DD` review date and now, or `null`.
|
|
444
|
+
*
|
|
445
|
+
* Every dollar figure Trazum prints descends from a price list, and the list
|
|
446
|
+
* carries the date it was checked. Printing only that date makes the reader do
|
|
447
|
+
* arithmetic against today to learn the one thing they wanted to know — whether
|
|
448
|
+
* to trust it — and a reader who is not already suspicious will not bother.
|
|
449
|
+
*
|
|
450
|
+
* `now` is a parameter for the reason `computeSavings` takes a `Date`: a function
|
|
451
|
+
* that reads the clock can only be asserted for shape.
|
|
452
|
+
*
|
|
453
|
+
* Compared at UTC midnight on both sides, so the answer does not change by one
|
|
454
|
+
* depending on what time of day the command runs. `null` for anything that is not
|
|
455
|
+
* a date — an overlay supplies this string, and a wrong one should read as
|
|
456
|
+
* unknown rather than as a confident number computed from `NaN`.
|
|
457
|
+
*/
|
|
458
|
+
export function reviewAgeDays(lastReviewed: string, now: Date): number | null {
|
|
459
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(lastReviewed)) return null;
|
|
460
|
+
const then = Date.parse(`${lastReviewed}T00:00:00Z`);
|
|
461
|
+
if (Number.isNaN(then)) return null;
|
|
462
|
+
|
|
463
|
+
const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
464
|
+
const days = Math.round((today - then) / 86_400_000);
|
|
465
|
+
// A future date is a typo or a clock problem, not an age. Reported as unknown
|
|
466
|
+
// rather than as a negative number of days, which reads like a bug either way.
|
|
467
|
+
return days < 0 ? null : days;
|
|
468
|
+
}
|
package/src/profile.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { segment } from './segment.js';
|
|
2
|
+
import { findExamples, findRestatedFormat } from './structure.js';
|
|
3
|
+
import { estimateTokens } from './tokenizer.js';
|
|
4
|
+
import type { TokenCounter } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a prompt is made of, so a team can decide which one to fix first.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is not a score
|
|
10
|
+
*
|
|
11
|
+
* The obvious shape for "which prompts should we optimise?" is a number out of
|
|
12
|
+
* a hundred, and it is the wrong shape. `Complexity: 74` cannot be argued with,
|
|
13
|
+
* cannot be reproduced by hand, and does not tell anybody what to do on Monday.
|
|
14
|
+
* Worse, the weights that turn four measurements into one number are invented —
|
|
15
|
+
* and once invented they get tuned until the ranking looks right, which is
|
|
16
|
+
* fitting the metric to the answer.
|
|
17
|
+
*
|
|
18
|
+
* So this returns **the measurements**, each with a definition you can check
|
|
19
|
+
* against the prompt in front of you, and ranks on the one quantity that is not
|
|
20
|
+
* a matter of opinion: **what optimising it would actually save.** That figure
|
|
21
|
+
* comes from running the deterministic rules, not from a formula.
|
|
22
|
+
*
|
|
23
|
+
* The structural facts are the *explanation* for a prompt's position in that
|
|
24
|
+
* ranking, not a substitute for it. "1,204 tokens across 8 sentences" says why
|
|
25
|
+
* a prompt is worth looking at; "$310 a month recoverable" says whether it is
|
|
26
|
+
* worth looking at before the other thirty-nine.
|
|
27
|
+
*
|
|
28
|
+
* ## What each measurement means, exactly
|
|
29
|
+
*
|
|
30
|
+
* - **sentences** — spans ending in `.`, `!`, `?`, `。`, `!`, `?`, or a line
|
|
31
|
+
* break where the line ends without punctuation (a bullet is a sentence).
|
|
32
|
+
* Protected content is excluded, so a code block is not forty sentences.
|
|
33
|
+
* - **tokensPerSentence** — the verbosity signal, and the only ratio here. It
|
|
34
|
+
* is length-independent: a padded 300-token prompt and a padded 3,000-token
|
|
35
|
+
* one look the same, which is the point. Reported with units, never as an
|
|
36
|
+
* index.
|
|
37
|
+
* - **examples / exampleTokens** — from `findExamples`, the same detector the
|
|
38
|
+
* advisories use. Few-shot examples are usually the largest single block in
|
|
39
|
+
* an expensive prompt and the easiest to trim by one.
|
|
40
|
+
* - **formatTokens** — a restated output format (a JSON schema written out
|
|
41
|
+
* twice, or written out at all when the API takes a schema parameter).
|
|
42
|
+
* - **protectedTokens** — code, URLs, placeholders and tags. Counted separately
|
|
43
|
+
* because **no amount of optimising will touch them**, and a prompt that is
|
|
44
|
+
* 80% code has far less headroom than its size suggests. Leaving this out is
|
|
45
|
+
* how a ranking sends somebody to spend an afternoon on a file that cannot
|
|
46
|
+
* move.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
export interface PromptProfile {
|
|
50
|
+
tokens: number;
|
|
51
|
+
/** Tokens that cannot be touched: code, URLs, placeholders, tags. */
|
|
52
|
+
protectedTokens: number;
|
|
53
|
+
sentences: number;
|
|
54
|
+
/** `tokens / sentences`, rounded to one decimal. Zero when there are none. */
|
|
55
|
+
tokensPerSentence: number;
|
|
56
|
+
examples: number;
|
|
57
|
+
exampleTokens: number;
|
|
58
|
+
/** Tokens in a restated output format, or 0. */
|
|
59
|
+
formatTokens: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const SENTENCE_END = /[.!?。!?]+/;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sentences in the mutable part of the prompt.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately simple and stated rather than clever. An abbreviation splits a
|
|
68
|
+
* sentence in two here, and that is accepted: the number is used as a
|
|
69
|
+
* denominator for a verbosity ratio, where a few percent of noise changes
|
|
70
|
+
* nothing, and the alternative is a sentence tokeniser this project would then
|
|
71
|
+
* have to defend.
|
|
72
|
+
*/
|
|
73
|
+
export function countSentences(prompt: string): number {
|
|
74
|
+
let total = 0;
|
|
75
|
+
|
|
76
|
+
for (const piece of segment(prompt)) {
|
|
77
|
+
if (piece.kind === 'protected') continue;
|
|
78
|
+
|
|
79
|
+
for (const line of piece.text.split('\n')) {
|
|
80
|
+
const trimmed = line.trim();
|
|
81
|
+
if (trimmed === '') continue;
|
|
82
|
+
|
|
83
|
+
const parts = trimmed
|
|
84
|
+
.split(SENTENCE_END)
|
|
85
|
+
.map((part) => part.trim())
|
|
86
|
+
.filter((part) => part !== '');
|
|
87
|
+
|
|
88
|
+
// A line with no terminal punctuation is still one thing being said — a
|
|
89
|
+
// bullet, a heading, a rule in a list. Counting it as zero would make
|
|
90
|
+
// every bulleted prompt look infinitely verbose.
|
|
91
|
+
total += Math.max(1, parts.length);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return total;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ProfileOptions {
|
|
99
|
+
count?: TokenCounter;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Measures a prompt. Deterministic, offline, and free. */
|
|
103
|
+
export function profilePrompt(prompt: string, options: ProfileOptions = {}): PromptProfile {
|
|
104
|
+
const count = options.count ?? estimateTokens;
|
|
105
|
+
|
|
106
|
+
const tokens = count(prompt);
|
|
107
|
+
const protectedTokens = segment(prompt)
|
|
108
|
+
.filter((piece) => piece.kind === 'protected')
|
|
109
|
+
.reduce((sum, piece) => sum + count(piece.text), 0);
|
|
110
|
+
|
|
111
|
+
const sentences = countSentences(prompt);
|
|
112
|
+
const examples = findExamples(prompt, count);
|
|
113
|
+
const format = findRestatedFormat(prompt, count);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
tokens,
|
|
117
|
+
protectedTokens,
|
|
118
|
+
sentences,
|
|
119
|
+
tokensPerSentence: sentences === 0 ? 0 : Math.round((tokens / sentences) * 10) / 10,
|
|
120
|
+
examples: examples.length,
|
|
121
|
+
exampleTokens: examples.reduce((sum, block) => sum + block.tokens, 0),
|
|
122
|
+
formatTokens: format?.restatedTokens ?? 0,
|
|
123
|
+
};
|
|
124
|
+
}
|