stagent 0.1.11 → 0.1.13
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 +74 -49
- package/package.json +3 -2
- package/public/readme/cost-usage-list.png +0 -0
- package/public/readme/dashboard-bulk-select.png +0 -0
- package/public/readme/dashboard-card-edit.png +0 -0
- package/public/readme/dashboard-create-form-ai-applied.png +0 -0
- package/public/readme/dashboard-create-form-ai-assist.png +0 -0
- package/public/readme/dashboard-create-form-empty.png +0 -0
- package/public/readme/dashboard-create-form-filled.png +0 -0
- package/public/readme/dashboard-filtered.png +0 -0
- package/public/readme/dashboard-list.png +0 -0
- package/public/readme/dashboard-workflow-confirm.png +0 -0
- package/public/readme/home-below-fold.png +0 -0
- package/public/readme/home-list.png +0 -0
- package/public/readme/inbox-list.png +0 -0
- package/public/readme/playbook-list.png +0 -0
- package/public/readme/profiles-list.png +0 -0
- package/public/readme/settings-list.png +0 -0
- package/public/readme/workflows-list.png +0 -0
- package/src/__tests__/e2e/blueprint.test.ts +63 -0
- package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
- package/src/__tests__/e2e/helpers.ts +286 -0
- package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
- package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
- package/src/__tests__/e2e/setup.ts +156 -0
- package/src/__tests__/e2e/single-task.test.ts +170 -0
- package/src/app/api/command-palette/recent/route.ts +41 -18
- package/src/app/api/context/batch/route.ts +44 -0
- package/src/app/api/permissions/presets/route.ts +80 -0
- package/src/app/api/playbook/status/route.ts +15 -0
- package/src/app/api/profiles/route.ts +23 -20
- package/src/app/api/settings/pricing/route.ts +15 -0
- package/src/app/api/tasks/[id]/route.ts +54 -3
- package/src/app/api/workflows/[id]/route.ts +43 -4
- package/src/app/api/workflows/[id]/status/route.ts +70 -2
- package/src/app/api/workflows/from-assist/route.ts +6 -32
- package/src/app/costs/page.tsx +53 -43
- package/src/app/dashboard/page.tsx +59 -21
- package/src/app/documents/[id]/page.tsx +10 -8
- package/src/app/globals.css +11 -0
- package/src/app/page.tsx +60 -3
- package/src/app/playbook/[slug]/page.tsx +76 -0
- package/src/app/playbook/page.tsx +54 -0
- package/src/app/profiles/page.tsx +7 -4
- package/src/app/settings/page.tsx +2 -2
- package/src/app/tasks/[id]/page.tsx +22 -2
- package/src/components/costs/cost-dashboard.tsx +226 -320
- package/src/components/dashboard/activity-feed.tsx +6 -2
- package/src/components/dashboard/greeting.tsx +3 -1
- package/src/components/dashboard/priority-queue.tsx +58 -9
- package/src/components/dashboard/stats-cards.tsx +16 -2
- package/src/components/documents/document-chip-bar.tsx +183 -0
- package/src/components/documents/document-content-renderer.tsx +146 -0
- package/src/components/documents/document-detail-view.tsx +16 -239
- package/src/components/documents/image-zoom-view.tsx +60 -0
- package/src/components/documents/smart-extracted-text.tsx +47 -0
- package/src/components/documents/utils.ts +70 -0
- package/src/components/notifications/batch-proposal-review.tsx +150 -0
- package/src/components/notifications/inbox-list.tsx +4 -5
- package/src/components/notifications/notification-item.tsx +73 -6
- package/src/components/notifications/pending-approval-host.tsx +63 -14
- package/src/components/playbook/adoption-heatmap.tsx +69 -0
- package/src/components/playbook/journey-card.tsx +110 -0
- package/src/components/playbook/playbook-action-button.tsx +22 -0
- package/src/components/playbook/playbook-browser.tsx +143 -0
- package/src/components/playbook/playbook-card.tsx +102 -0
- package/src/components/playbook/playbook-detail-view.tsx +225 -0
- package/src/components/playbook/playbook-homepage.tsx +142 -0
- package/src/components/playbook/playbook-toc.tsx +90 -0
- package/src/components/playbook/playbook-updated-badge.tsx +23 -0
- package/src/components/playbook/related-docs.tsx +30 -0
- package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
- package/src/components/profiles/context-proposal-review.tsx +7 -3
- package/src/components/profiles/learned-context-panel.tsx +116 -8
- package/src/components/profiles/profile-browser.tsx +1 -0
- package/src/components/profiles/profile-card.tsx +16 -8
- package/src/components/profiles/profile-detail-view.tsx +12 -4
- package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
- package/src/components/settings/api-key-form.tsx +5 -43
- package/src/components/settings/auth-config-section.tsx +10 -6
- package/src/components/settings/auth-status-badge.tsx +8 -0
- package/src/components/settings/budget-guardrails-section.tsx +403 -620
- package/src/components/settings/connection-test-control.tsx +63 -0
- package/src/components/settings/permissions-section.tsx +85 -75
- package/src/components/settings/permissions-sections.tsx +24 -0
- package/src/components/settings/presets-section.tsx +159 -0
- package/src/components/settings/pricing-registry-panel.tsx +164 -0
- package/src/components/shared/app-sidebar.tsx +4 -2
- package/src/components/shared/command-palette.tsx +30 -0
- package/src/components/shared/light-markdown.tsx +134 -0
- package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +1 -1
- package/src/components/tasks/ai-assist-panel.tsx +108 -78
- package/src/components/tasks/content-preview.tsx +2 -1
- package/src/components/tasks/kanban-board.tsx +57 -5
- package/src/components/tasks/kanban-column.tsx +34 -23
- package/src/components/tasks/task-bento-cell.tsx +50 -0
- package/src/components/tasks/task-bento-grid.tsx +155 -0
- package/src/components/tasks/task-card.tsx +14 -16
- package/src/components/tasks/task-chip-bar.tsx +207 -0
- package/src/components/tasks/task-detail-view.tsx +42 -190
- package/src/components/tasks/task-result-renderer.tsx +33 -0
- package/src/components/workflows/blueprint-gallery.tsx +19 -12
- package/src/components/workflows/blueprint-preview.tsx +8 -1
- package/src/components/workflows/loop-status-view.tsx +2 -4
- package/src/components/workflows/swarm-dashboard.tsx +2 -3
- package/src/components/workflows/workflow-confirmation-view.tsx +2 -7
- package/src/components/workflows/workflow-full-output.tsx +80 -0
- package/src/components/workflows/workflow-kanban-card.tsx +121 -0
- package/src/components/workflows/workflow-list.tsx +47 -42
- package/src/components/workflows/workflow-status-view.tsx +163 -16
- package/src/lib/agents/learned-context.ts +27 -15
- package/src/lib/agents/learning-session.ts +354 -0
- package/src/lib/agents/pattern-extractor.ts +19 -0
- package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
- package/src/lib/agents/profiles/sort.ts +7 -0
- package/src/lib/constants/card-icons.tsx +202 -0
- package/src/lib/constants/prose-styles.ts +7 -0
- package/src/lib/constants/settings.ts +1 -0
- package/src/lib/constants/task-status.ts +3 -0
- package/src/lib/db/schema.ts +3 -0
- package/src/lib/docs/adoption.ts +105 -0
- package/src/lib/docs/journey-tracker.ts +21 -0
- package/src/lib/docs/reader.ts +107 -0
- package/src/lib/docs/types.ts +54 -0
- package/src/lib/docs/usage-stage.ts +60 -0
- package/src/lib/documents/context-builder.ts +41 -0
- package/src/lib/notifications/actionable.ts +18 -10
- package/src/lib/queries/chart-data.ts +20 -1
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
- package/src/lib/settings/budget-guardrails.ts +213 -85
- package/src/lib/settings/permission-presets.ts +150 -0
- package/src/lib/settings/runtime-setup.ts +71 -0
- package/src/lib/usage/__tests__/ledger.test.ts +2 -2
- package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
- package/src/lib/usage/ledger.ts +1 -1
- package/src/lib/usage/pricing-registry.ts +570 -0
- package/src/lib/usage/pricing.ts +15 -95
- package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
- package/src/lib/utils/learned-context-history.ts +150 -0
- package/src/lib/validators/__tests__/settings.test.ts +23 -16
- package/src/lib/validators/settings.ts +3 -9
- package/src/lib/workflows/engine.ts +75 -61
- package/src/lib/workflows/types.ts +2 -0
- package/tsconfig.json +2 -1
- package/src/components/documents/document-preview.tsx +0 -68
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { SETTINGS_KEYS } from "@/lib/constants/settings";
|
|
3
|
+
import { getSetting, setSetting } from "@/lib/settings/helpers";
|
|
4
|
+
import type { ClaudeOAuthPlan } from "@/lib/validators/settings";
|
|
5
|
+
|
|
6
|
+
export type PricingProviderId = "anthropic" | "openai";
|
|
7
|
+
export type PricingRowKind = "api_model" | "subscription_plan";
|
|
8
|
+
export type PricingSourceType = "bundled_default" | "official_pricing_page";
|
|
9
|
+
|
|
10
|
+
export interface PricingRow {
|
|
11
|
+
key: string;
|
|
12
|
+
providerId: PricingProviderId;
|
|
13
|
+
kind: PricingRowKind;
|
|
14
|
+
label: string;
|
|
15
|
+
visible: boolean;
|
|
16
|
+
matchPrefixes: string[];
|
|
17
|
+
inputCostPerMillionMicros: number | null;
|
|
18
|
+
outputCostPerMillionMicros: number | null;
|
|
19
|
+
monthlyPriceUsd: number | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProviderPricingSnapshot {
|
|
23
|
+
providerId: PricingProviderId;
|
|
24
|
+
sourceType: PricingSourceType;
|
|
25
|
+
sourceLabel: string;
|
|
26
|
+
fetchedAtIso: string | null;
|
|
27
|
+
version: string;
|
|
28
|
+
refreshError: string | null;
|
|
29
|
+
rows: PricingRow[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PricingRegistry {
|
|
33
|
+
providers: Record<PricingProviderId, ProviderPricingSnapshot>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PricingRegistrySnapshot extends PricingRegistry {
|
|
37
|
+
lastUpdatedIso: string | null;
|
|
38
|
+
stale: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const STALE_AFTER_MS = 1000 * 60 * 60 * 24 * 7;
|
|
42
|
+
|
|
43
|
+
const DEFAULT_ROWS: PricingRow[] = [
|
|
44
|
+
{
|
|
45
|
+
key: "anthropic-claude-opus",
|
|
46
|
+
providerId: "anthropic",
|
|
47
|
+
kind: "api_model",
|
|
48
|
+
label: "Claude Opus",
|
|
49
|
+
visible: true,
|
|
50
|
+
matchPrefixes: ["claude-opus"],
|
|
51
|
+
inputCostPerMillionMicros: 5_000_000,
|
|
52
|
+
outputCostPerMillionMicros: 25_000_000,
|
|
53
|
+
monthlyPriceUsd: null,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: "anthropic-claude-sonnet",
|
|
57
|
+
providerId: "anthropic",
|
|
58
|
+
kind: "api_model",
|
|
59
|
+
label: "Claude Sonnet",
|
|
60
|
+
visible: true,
|
|
61
|
+
matchPrefixes: ["claude-sonnet"],
|
|
62
|
+
inputCostPerMillionMicros: 3_000_000,
|
|
63
|
+
outputCostPerMillionMicros: 15_000_000,
|
|
64
|
+
monthlyPriceUsd: null,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: "anthropic-claude-haiku",
|
|
68
|
+
providerId: "anthropic",
|
|
69
|
+
kind: "api_model",
|
|
70
|
+
label: "Claude Haiku",
|
|
71
|
+
visible: true,
|
|
72
|
+
matchPrefixes: ["claude-haiku"],
|
|
73
|
+
inputCostPerMillionMicros: 1_000_000,
|
|
74
|
+
outputCostPerMillionMicros: 5_000_000,
|
|
75
|
+
monthlyPriceUsd: null,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: "anthropic-claude-fallback",
|
|
79
|
+
providerId: "anthropic",
|
|
80
|
+
kind: "api_model",
|
|
81
|
+
label: "Anthropic Fallback",
|
|
82
|
+
visible: false,
|
|
83
|
+
matchPrefixes: [],
|
|
84
|
+
inputCostPerMillionMicros: 5_000_000,
|
|
85
|
+
outputCostPerMillionMicros: 25_000_000,
|
|
86
|
+
monthlyPriceUsd: null,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: "anthropic-plan-pro",
|
|
90
|
+
providerId: "anthropic",
|
|
91
|
+
kind: "subscription_plan",
|
|
92
|
+
label: "Claude Pro",
|
|
93
|
+
visible: true,
|
|
94
|
+
matchPrefixes: [],
|
|
95
|
+
inputCostPerMillionMicros: null,
|
|
96
|
+
outputCostPerMillionMicros: null,
|
|
97
|
+
monthlyPriceUsd: 20,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: "anthropic-plan-max-5x",
|
|
101
|
+
providerId: "anthropic",
|
|
102
|
+
kind: "subscription_plan",
|
|
103
|
+
label: "Claude Max 5x",
|
|
104
|
+
visible: true,
|
|
105
|
+
matchPrefixes: [],
|
|
106
|
+
inputCostPerMillionMicros: null,
|
|
107
|
+
outputCostPerMillionMicros: null,
|
|
108
|
+
monthlyPriceUsd: 100,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: "anthropic-plan-max-20x",
|
|
112
|
+
providerId: "anthropic",
|
|
113
|
+
kind: "subscription_plan",
|
|
114
|
+
label: "Claude Max 20x",
|
|
115
|
+
visible: true,
|
|
116
|
+
matchPrefixes: [],
|
|
117
|
+
inputCostPerMillionMicros: null,
|
|
118
|
+
outputCostPerMillionMicros: null,
|
|
119
|
+
monthlyPriceUsd: 200,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: "openai-codex-mini",
|
|
123
|
+
providerId: "openai",
|
|
124
|
+
kind: "api_model",
|
|
125
|
+
label: "Codex Mini",
|
|
126
|
+
visible: true,
|
|
127
|
+
matchPrefixes: ["codex-mini"],
|
|
128
|
+
inputCostPerMillionMicros: 1_500_000,
|
|
129
|
+
outputCostPerMillionMicros: 6_000_000,
|
|
130
|
+
monthlyPriceUsd: null,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "openai-gpt-5",
|
|
134
|
+
providerId: "openai",
|
|
135
|
+
kind: "api_model",
|
|
136
|
+
label: "GPT-5",
|
|
137
|
+
visible: true,
|
|
138
|
+
matchPrefixes: ["gpt-5", "o3", "o4"],
|
|
139
|
+
inputCostPerMillionMicros: 10_000_000,
|
|
140
|
+
outputCostPerMillionMicros: 30_000_000,
|
|
141
|
+
monthlyPriceUsd: null,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
key: "openai-gpt-4o",
|
|
145
|
+
providerId: "openai",
|
|
146
|
+
kind: "api_model",
|
|
147
|
+
label: "GPT-4o",
|
|
148
|
+
visible: true,
|
|
149
|
+
matchPrefixes: ["gpt-4o"],
|
|
150
|
+
inputCostPerMillionMicros: 2_500_000,
|
|
151
|
+
outputCostPerMillionMicros: 10_000_000,
|
|
152
|
+
monthlyPriceUsd: null,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: "openai-fallback",
|
|
156
|
+
providerId: "openai",
|
|
157
|
+
kind: "api_model",
|
|
158
|
+
label: "OpenAI Fallback",
|
|
159
|
+
visible: false,
|
|
160
|
+
matchPrefixes: [],
|
|
161
|
+
inputCostPerMillionMicros: 10_000_000,
|
|
162
|
+
outputCostPerMillionMicros: 30_000_000,
|
|
163
|
+
monthlyPriceUsd: null,
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
type ProviderDefaults = Record<PricingProviderId, ProviderPricingSnapshot>;
|
|
168
|
+
|
|
169
|
+
function buildDefaultProviders(): ProviderDefaults {
|
|
170
|
+
const nowIso = "2026-03-17T00:00:00.000Z";
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
anthropic: {
|
|
174
|
+
providerId: "anthropic",
|
|
175
|
+
sourceType: "bundled_default",
|
|
176
|
+
sourceLabel: "Bundled Anthropic pricing defaults",
|
|
177
|
+
fetchedAtIso: nowIso,
|
|
178
|
+
version: "bundled-2026-03-17",
|
|
179
|
+
refreshError: null,
|
|
180
|
+
rows: DEFAULT_ROWS.filter((row) => row.providerId === "anthropic"),
|
|
181
|
+
},
|
|
182
|
+
openai: {
|
|
183
|
+
providerId: "openai",
|
|
184
|
+
sourceType: "bundled_default",
|
|
185
|
+
sourceLabel: "Bundled OpenAI pricing defaults",
|
|
186
|
+
fetchedAtIso: nowIso,
|
|
187
|
+
version: "bundled-2026-03-17",
|
|
188
|
+
refreshError: null,
|
|
189
|
+
rows: DEFAULT_ROWS.filter((row) => row.providerId === "openai"),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function cloneRegistry(registry: PricingRegistry): PricingRegistry {
|
|
195
|
+
return {
|
|
196
|
+
providers: {
|
|
197
|
+
anthropic: {
|
|
198
|
+
...registry.providers.anthropic,
|
|
199
|
+
rows: registry.providers.anthropic.rows.map((row) => ({ ...row })),
|
|
200
|
+
},
|
|
201
|
+
openai: {
|
|
202
|
+
...registry.providers.openai,
|
|
203
|
+
rows: registry.providers.openai.rows.map((row) => ({ ...row })),
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function buildDefaultRegistry(): PricingRegistry {
|
|
210
|
+
return { providers: buildDefaultProviders() };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isPricingProviderId(value: string): value is PricingProviderId {
|
|
214
|
+
return value === "anthropic" || value === "openai";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isPricingRowKind(value: string): value is PricingRowKind {
|
|
218
|
+
return value === "api_model" || value === "subscription_plan";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parsePricingRegistry(value: string | null): PricingRegistry | null {
|
|
222
|
+
if (!value) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const parsed = JSON.parse(value) as PricingRegistry;
|
|
228
|
+
if (!parsed || typeof parsed !== "object" || !parsed.providers) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const next = buildDefaultRegistry();
|
|
233
|
+
|
|
234
|
+
for (const providerId of ["anthropic", "openai"] as const) {
|
|
235
|
+
const provider = parsed.providers[providerId];
|
|
236
|
+
if (!provider) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const sourceType = provider.sourceType;
|
|
241
|
+
next.providers[providerId] = {
|
|
242
|
+
providerId,
|
|
243
|
+
sourceType:
|
|
244
|
+
sourceType === "official_pricing_page" ? sourceType : "bundled_default",
|
|
245
|
+
sourceLabel:
|
|
246
|
+
typeof provider.sourceLabel === "string"
|
|
247
|
+
? provider.sourceLabel
|
|
248
|
+
: next.providers[providerId].sourceLabel,
|
|
249
|
+
fetchedAtIso:
|
|
250
|
+
typeof provider.fetchedAtIso === "string" ? provider.fetchedAtIso : null,
|
|
251
|
+
version:
|
|
252
|
+
typeof provider.version === "string"
|
|
253
|
+
? provider.version
|
|
254
|
+
: next.providers[providerId].version,
|
|
255
|
+
refreshError:
|
|
256
|
+
typeof provider.refreshError === "string" ? provider.refreshError : null,
|
|
257
|
+
rows: Array.isArray(provider.rows)
|
|
258
|
+
? provider.rows
|
|
259
|
+
.filter(
|
|
260
|
+
(row): row is PricingRow =>
|
|
261
|
+
Boolean(row) &&
|
|
262
|
+
typeof row === "object" &&
|
|
263
|
+
typeof row.key === "string" &&
|
|
264
|
+
isPricingProviderId(row.providerId) &&
|
|
265
|
+
isPricingRowKind(row.kind) &&
|
|
266
|
+
typeof row.label === "string" &&
|
|
267
|
+
typeof row.visible === "boolean" &&
|
|
268
|
+
Array.isArray(row.matchPrefixes)
|
|
269
|
+
)
|
|
270
|
+
.map((row) => ({
|
|
271
|
+
...row,
|
|
272
|
+
matchPrefixes: row.matchPrefixes.filter(
|
|
273
|
+
(prefix): prefix is string => typeof prefix === "string"
|
|
274
|
+
),
|
|
275
|
+
}))
|
|
276
|
+
: next.providers[providerId].rows,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return next;
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function normalizeText(html: string) {
|
|
287
|
+
return html
|
|
288
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
289
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
290
|
+
.replace(/<[^>]+>/g, " ")
|
|
291
|
+
.replace(/ /gi, " ")
|
|
292
|
+
.replace(/'/gi, "'")
|
|
293
|
+
.replace(/&/gi, "&")
|
|
294
|
+
.replace(/\s+/g, " ")
|
|
295
|
+
.trim();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function escapePattern(value: string) {
|
|
299
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function priceToMicros(value: number) {
|
|
303
|
+
return Math.round(value * 1_000_000);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function extractModelPricing(
|
|
307
|
+
text: string,
|
|
308
|
+
labels: string[]
|
|
309
|
+
): { inputMicros: number; outputMicros: number } | null {
|
|
310
|
+
for (const label of labels) {
|
|
311
|
+
const pattern = new RegExp(
|
|
312
|
+
`${escapePattern(label)}[\\s\\S]{0,260}?\\$(\\d+(?:\\.\\d+)?)\\s*\\/\\s*1M[\\s\\S]{0,160}?\\$(\\d+(?:\\.\\d+)?)\\s*\\/\\s*1M`,
|
|
313
|
+
"i"
|
|
314
|
+
);
|
|
315
|
+
const match = text.match(pattern);
|
|
316
|
+
if (match?.[1] && match?.[2]) {
|
|
317
|
+
return {
|
|
318
|
+
inputMicros: priceToMicros(Number(match[1])),
|
|
319
|
+
outputMicros: priceToMicros(Number(match[2])),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function extractPlanPrice(text: string, labels: string[]): number | null {
|
|
328
|
+
for (const label of labels) {
|
|
329
|
+
const pattern = new RegExp(
|
|
330
|
+
`${escapePattern(label)}[\\s\\S]{0,120}?\\$(\\d+(?:\\.\\d+)?)`,
|
|
331
|
+
"i"
|
|
332
|
+
);
|
|
333
|
+
const match = text.match(pattern);
|
|
334
|
+
if (match?.[1]) {
|
|
335
|
+
return Number(match[1]);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function buildVersion(providerId: PricingProviderId, rows: PricingRow[]) {
|
|
343
|
+
const payload = rows
|
|
344
|
+
.filter((row) => row.visible)
|
|
345
|
+
.map((row) => ({
|
|
346
|
+
key: row.key,
|
|
347
|
+
inputCostPerMillionMicros: row.inputCostPerMillionMicros,
|
|
348
|
+
outputCostPerMillionMicros: row.outputCostPerMillionMicros,
|
|
349
|
+
monthlyPriceUsd: row.monthlyPriceUsd,
|
|
350
|
+
}));
|
|
351
|
+
const hash = createHash("sha256")
|
|
352
|
+
.update(JSON.stringify(payload))
|
|
353
|
+
.digest("hex")
|
|
354
|
+
.slice(0, 10);
|
|
355
|
+
return `${providerId}-registry-${hash}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function updateRow(
|
|
359
|
+
rows: PricingRow[],
|
|
360
|
+
key: string,
|
|
361
|
+
values: Partial<Pick<PricingRow, "inputCostPerMillionMicros" | "outputCostPerMillionMicros" | "monthlyPriceUsd">>
|
|
362
|
+
) {
|
|
363
|
+
const row = rows.find((entry) => entry.key === key);
|
|
364
|
+
if (!row) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (values.inputCostPerMillionMicros != null) {
|
|
369
|
+
row.inputCostPerMillionMicros = values.inputCostPerMillionMicros;
|
|
370
|
+
}
|
|
371
|
+
if (values.outputCostPerMillionMicros != null) {
|
|
372
|
+
row.outputCostPerMillionMicros = values.outputCostPerMillionMicros;
|
|
373
|
+
}
|
|
374
|
+
if (values.monthlyPriceUsd != null) {
|
|
375
|
+
row.monthlyPriceUsd = values.monthlyPriceUsd;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function refreshAnthropicPricing(
|
|
380
|
+
current: ProviderPricingSnapshot
|
|
381
|
+
): Promise<ProviderPricingSnapshot> {
|
|
382
|
+
const response = await fetch("https://www.anthropic.com/pricing", {
|
|
383
|
+
cache: "no-store",
|
|
384
|
+
});
|
|
385
|
+
if (!response.ok) {
|
|
386
|
+
throw new Error(`Anthropic pricing fetch failed (${response.status})`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const text = normalizeText(await response.text());
|
|
390
|
+
const rows = current.rows.map((row) => ({ ...row }));
|
|
391
|
+
|
|
392
|
+
const opus = extractModelPricing(text, ["Claude Opus 4.6", "Claude Opus 4.5", "Claude Opus 4"]);
|
|
393
|
+
if (opus) {
|
|
394
|
+
updateRow(rows, "anthropic-claude-opus", {
|
|
395
|
+
inputCostPerMillionMicros: opus.inputMicros,
|
|
396
|
+
outputCostPerMillionMicros: opus.outputMicros,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const sonnet = extractModelPricing(text, ["Claude Sonnet 4.6", "Claude Sonnet 4.5", "Claude Sonnet 4"]);
|
|
401
|
+
if (sonnet) {
|
|
402
|
+
updateRow(rows, "anthropic-claude-sonnet", {
|
|
403
|
+
inputCostPerMillionMicros: sonnet.inputMicros,
|
|
404
|
+
outputCostPerMillionMicros: sonnet.outputMicros,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const haiku = extractModelPricing(text, ["Claude Haiku 4.5", "Claude Haiku 3.5", "Claude Haiku"]);
|
|
409
|
+
if (haiku) {
|
|
410
|
+
updateRow(rows, "anthropic-claude-haiku", {
|
|
411
|
+
inputCostPerMillionMicros: haiku.inputMicros,
|
|
412
|
+
outputCostPerMillionMicros: haiku.outputMicros,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const pro = extractPlanPrice(text, ["Claude Pro", "Pro"]);
|
|
417
|
+
if (pro != null) {
|
|
418
|
+
updateRow(rows, "anthropic-plan-pro", { monthlyPriceUsd: pro });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const max5x = extractPlanPrice(text, ["Max 5x", "Claude Max 5x"]);
|
|
422
|
+
if (max5x != null) {
|
|
423
|
+
updateRow(rows, "anthropic-plan-max-5x", { monthlyPriceUsd: max5x });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const max20x = extractPlanPrice(text, ["Max 20x", "Claude Max 20x"]);
|
|
427
|
+
if (max20x != null) {
|
|
428
|
+
updateRow(rows, "anthropic-plan-max-20x", { monthlyPriceUsd: max20x });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const fetchedAtIso = new Date().toISOString();
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
providerId: "anthropic",
|
|
435
|
+
sourceType: "official_pricing_page",
|
|
436
|
+
sourceLabel: "Anthropic pricing page",
|
|
437
|
+
fetchedAtIso,
|
|
438
|
+
version: buildVersion("anthropic", rows),
|
|
439
|
+
refreshError: null,
|
|
440
|
+
rows,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function refreshOpenAIPricing(
|
|
445
|
+
current: ProviderPricingSnapshot
|
|
446
|
+
): Promise<ProviderPricingSnapshot> {
|
|
447
|
+
const response = await fetch("https://openai.com/api/pricing/", {
|
|
448
|
+
cache: "no-store",
|
|
449
|
+
});
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
throw new Error(`OpenAI pricing fetch failed (${response.status})`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const text = normalizeText(await response.text());
|
|
455
|
+
const rows = current.rows.map((row) => ({ ...row }));
|
|
456
|
+
|
|
457
|
+
const gpt5 = extractModelPricing(text, ["GPT-5.4", "GPT-5.1", "GPT-5"]);
|
|
458
|
+
if (gpt5) {
|
|
459
|
+
updateRow(rows, "openai-gpt-5", {
|
|
460
|
+
inputCostPerMillionMicros: gpt5.inputMicros,
|
|
461
|
+
outputCostPerMillionMicros: gpt5.outputMicros,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const gpt4o = extractModelPricing(text, ["GPT-4o"]);
|
|
466
|
+
if (gpt4o) {
|
|
467
|
+
updateRow(rows, "openai-gpt-4o", {
|
|
468
|
+
inputCostPerMillionMicros: gpt4o.inputMicros,
|
|
469
|
+
outputCostPerMillionMicros: gpt4o.outputMicros,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const fetchedAtIso = new Date().toISOString();
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
providerId: "openai",
|
|
477
|
+
sourceType: "official_pricing_page",
|
|
478
|
+
sourceLabel: "OpenAI API pricing page",
|
|
479
|
+
fetchedAtIso,
|
|
480
|
+
version: buildVersion("openai", rows),
|
|
481
|
+
refreshError: null,
|
|
482
|
+
rows,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export async function getPricingRegistry(): Promise<PricingRegistry> {
|
|
487
|
+
const stored = parsePricingRegistry(await getSetting(SETTINGS_KEYS.PRICING_REGISTRY));
|
|
488
|
+
return stored ?? buildDefaultRegistry();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export async function setPricingRegistry(registry: PricingRegistry) {
|
|
492
|
+
await setSetting(SETTINGS_KEYS.PRICING_REGISTRY, JSON.stringify(registry));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export async function getPricingRegistrySnapshot(): Promise<PricingRegistrySnapshot> {
|
|
496
|
+
const registry = await getPricingRegistry();
|
|
497
|
+
const fetchedAtValues = Object.values(registry.providers)
|
|
498
|
+
.map((provider) => provider.fetchedAtIso)
|
|
499
|
+
.filter((value): value is string => Boolean(value));
|
|
500
|
+
const lastUpdatedIso =
|
|
501
|
+
fetchedAtValues.length > 0
|
|
502
|
+
? fetchedAtValues.sort((left, right) => right.localeCompare(left))[0]
|
|
503
|
+
: null;
|
|
504
|
+
const stale =
|
|
505
|
+
lastUpdatedIso == null
|
|
506
|
+
? true
|
|
507
|
+
: Date.now() - new Date(lastUpdatedIso).getTime() > STALE_AFTER_MS;
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
...cloneRegistry(registry),
|
|
511
|
+
lastUpdatedIso,
|
|
512
|
+
stale,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export async function refreshPricingRegistry(): Promise<PricingRegistrySnapshot> {
|
|
517
|
+
const current = await getPricingRegistry();
|
|
518
|
+
const next = cloneRegistry(current);
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
next.providers.anthropic = await refreshAnthropicPricing(next.providers.anthropic);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
next.providers.anthropic.refreshError =
|
|
524
|
+
error instanceof Error ? error.message : "Failed to refresh Anthropic pricing";
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
next.providers.openai = await refreshOpenAIPricing(next.providers.openai);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
next.providers.openai.refreshError =
|
|
531
|
+
error instanceof Error ? error.message : "Failed to refresh OpenAI pricing";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
await setPricingRegistry(next);
|
|
535
|
+
return getPricingRegistrySnapshot();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export async function findPricingRowForModel(input: {
|
|
539
|
+
providerId: PricingProviderId;
|
|
540
|
+
modelId: string;
|
|
541
|
+
}) {
|
|
542
|
+
const registry = await getPricingRegistry();
|
|
543
|
+
const rows = registry.providers[input.providerId].rows.filter(
|
|
544
|
+
(row) => row.kind === "api_model"
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const matched = rows.find((row) =>
|
|
548
|
+
row.matchPrefixes.some((prefix) => input.modelId.startsWith(prefix))
|
|
549
|
+
);
|
|
550
|
+
if (matched) {
|
|
551
|
+
return matched;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return rows.find((row) => row.key === `${input.providerId}-fallback`) ?? null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export async function getClaudeOAuthPlanPrice(plan: ClaudeOAuthPlan | undefined) {
|
|
558
|
+
const registry = await getPricingRegistry();
|
|
559
|
+
const key =
|
|
560
|
+
plan === "max_5x"
|
|
561
|
+
? "anthropic-plan-max-5x"
|
|
562
|
+
: plan === "max_20x"
|
|
563
|
+
? "anthropic-plan-max-20x"
|
|
564
|
+
: "anthropic-plan-pro";
|
|
565
|
+
|
|
566
|
+
return (
|
|
567
|
+
registry.providers.anthropic.rows.find((row) => row.key === key)?.monthlyPriceUsd ??
|
|
568
|
+
20
|
|
569
|
+
);
|
|
570
|
+
}
|
package/src/lib/usage/pricing.ts
CHANGED
|
@@ -1,122 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
providerId: "anthropic" | "openai";
|
|
3
|
-
pricingVersion: string;
|
|
4
|
-
inputCostPerMillionMicros: number;
|
|
5
|
-
outputCostPerMillionMicros: number;
|
|
6
|
-
matchesModel(modelId: string): boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const PRICING_RULES: PricingRule[] = [
|
|
10
|
-
// ── Anthropic ──────────────────────────────────────────────────────
|
|
11
|
-
{
|
|
12
|
-
providerId: "anthropic",
|
|
13
|
-
pricingVersion: "registry-2026-03-15",
|
|
14
|
-
inputCostPerMillionMicros: 15_000_000,
|
|
15
|
-
outputCostPerMillionMicros: 75_000_000,
|
|
16
|
-
matchesModel(modelId) {
|
|
17
|
-
return modelId.startsWith("claude-opus");
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
providerId: "anthropic",
|
|
22
|
-
pricingVersion: "registry-2026-03-15",
|
|
23
|
-
inputCostPerMillionMicros: 3_000_000,
|
|
24
|
-
outputCostPerMillionMicros: 15_000_000,
|
|
25
|
-
matchesModel(modelId) {
|
|
26
|
-
return modelId.startsWith("claude-sonnet");
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
providerId: "anthropic",
|
|
31
|
-
pricingVersion: "registry-2026-03-15",
|
|
32
|
-
inputCostPerMillionMicros: 800_000,
|
|
33
|
-
outputCostPerMillionMicros: 4_000_000,
|
|
34
|
-
matchesModel(modelId) {
|
|
35
|
-
return modelId.startsWith("claude-haiku");
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
// ── OpenAI ─────────────────────────────────────────────────────────
|
|
39
|
-
{
|
|
40
|
-
providerId: "openai",
|
|
41
|
-
pricingVersion: "registry-2026-03-15",
|
|
42
|
-
inputCostPerMillionMicros: 1_500_000,
|
|
43
|
-
outputCostPerMillionMicros: 6_000_000,
|
|
44
|
-
matchesModel(modelId) {
|
|
45
|
-
return modelId.startsWith("codex-mini") || modelId === "codex-mini-latest";
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
providerId: "openai",
|
|
50
|
-
pricingVersion: "registry-2026-03-15",
|
|
51
|
-
inputCostPerMillionMicros: 2_500_000,
|
|
52
|
-
outputCostPerMillionMicros: 10_000_000,
|
|
53
|
-
matchesModel(modelId) {
|
|
54
|
-
return modelId.startsWith("gpt-4o");
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
providerId: "openai",
|
|
59
|
-
pricingVersion: "registry-2026-03-15",
|
|
60
|
-
inputCostPerMillionMicros: 10_000_000,
|
|
61
|
-
outputCostPerMillionMicros: 30_000_000,
|
|
62
|
-
matchesModel(modelId) {
|
|
63
|
-
return modelId.startsWith("gpt-5") || modelId.startsWith("o3") || modelId.startsWith("o4");
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
// ── Catch-all (conservative estimate to prevent null costs) ────────
|
|
67
|
-
{
|
|
68
|
-
providerId: "anthropic",
|
|
69
|
-
pricingVersion: "registry-2026-03-15-fallback",
|
|
70
|
-
inputCostPerMillionMicros: 15_000_000,
|
|
71
|
-
outputCostPerMillionMicros: 75_000_000,
|
|
72
|
-
matchesModel() {
|
|
73
|
-
return true;
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
providerId: "openai",
|
|
78
|
-
pricingVersion: "registry-2026-03-15-fallback",
|
|
79
|
-
inputCostPerMillionMicros: 10_000_000,
|
|
80
|
-
outputCostPerMillionMicros: 30_000_000,
|
|
81
|
-
matchesModel() {
|
|
82
|
-
return true;
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
];
|
|
1
|
+
import { findPricingRowForModel } from "./pricing-registry";
|
|
86
2
|
|
|
87
3
|
export interface DerivedCost {
|
|
88
4
|
costMicros: number | null;
|
|
89
5
|
pricingVersion: string | null;
|
|
90
6
|
}
|
|
91
7
|
|
|
92
|
-
export function deriveUsageCostMicros(input: {
|
|
8
|
+
export async function deriveUsageCostMicros(input: {
|
|
93
9
|
providerId: string;
|
|
94
10
|
modelId?: string | null;
|
|
95
11
|
inputTokens?: number | null;
|
|
96
12
|
outputTokens?: number | null;
|
|
97
|
-
}): DerivedCost {
|
|
13
|
+
}): Promise<DerivedCost> {
|
|
98
14
|
if (!input.modelId) {
|
|
99
15
|
return { costMicros: null, pricingVersion: null };
|
|
100
16
|
}
|
|
101
17
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
18
|
+
if (input.providerId !== "anthropic" && input.providerId !== "openai") {
|
|
19
|
+
return { costMicros: null, pricingVersion: null };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const row = await findPricingRowForModel({
|
|
23
|
+
providerId: input.providerId,
|
|
24
|
+
modelId: input.modelId,
|
|
25
|
+
});
|
|
106
26
|
|
|
107
|
-
if (!
|
|
27
|
+
if (!row) {
|
|
108
28
|
return { costMicros: null, pricingVersion: null };
|
|
109
29
|
}
|
|
110
30
|
|
|
111
31
|
const inputTokens = input.inputTokens ?? 0;
|
|
112
32
|
const outputTokens = input.outputTokens ?? 0;
|
|
113
33
|
const inputCost =
|
|
114
|
-
(inputTokens *
|
|
34
|
+
(inputTokens * (row.inputCostPerMillionMicros ?? 0)) / 1_000_000;
|
|
115
35
|
const outputCost =
|
|
116
|
-
(outputTokens *
|
|
36
|
+
(outputTokens * (row.outputCostPerMillionMicros ?? 0)) / 1_000_000;
|
|
117
37
|
|
|
118
38
|
return {
|
|
119
39
|
costMicros: Math.round(inputCost + outputCost),
|
|
120
|
-
pricingVersion:
|
|
40
|
+
pricingVersion: row.key,
|
|
121
41
|
};
|
|
122
42
|
}
|