@agent-finops/core 0.1.5 → 0.2.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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/planDetection.d.ts +41 -0
- package/dist/planDetection.js +139 -0
- package/dist/planMath.d.ts +11 -1
- package/dist/planMath.js +51 -10
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./toolInvocations.js";
|
|
|
9
9
|
export * from "./insights.js";
|
|
10
10
|
export * from "./localAgentLogs.js";
|
|
11
11
|
export * from "./modelPricing.js";
|
|
12
|
+
export * from "./planDetection.js";
|
|
12
13
|
export * from "./planMath.js";
|
|
13
14
|
export * from "./sampleData.js";
|
|
14
15
|
export * from "./scanGuard.js";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./toolInvocations.js";
|
|
|
9
9
|
export * from "./insights.js";
|
|
10
10
|
export * from "./localAgentLogs.js";
|
|
11
11
|
export * from "./modelPricing.js";
|
|
12
|
+
export * from "./planDetection.js";
|
|
12
13
|
export * from "./planMath.js";
|
|
13
14
|
export * from "./sampleData.js";
|
|
14
15
|
export * from "./scanGuard.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local subscription-plan detection — the zero-friction, fully legitimate
|
|
3
|
+
* path to "the tool knows who it's talking to".
|
|
4
|
+
*
|
|
5
|
+
* Consumer subscriptions (Claude Max/Pro, ChatGPT Plus/Pro) have NO billing
|
|
6
|
+
* API, so there is nothing to OAuth against. But the coding agents already
|
|
7
|
+
* did their own OAuth and persist what they learned on disk, right next to
|
|
8
|
+
* the transcripts we already read:
|
|
9
|
+
*
|
|
10
|
+
* - Claude Code: `~/.claude.json` -> `oauthAccount.organizationType`
|
|
11
|
+
* ("claude_max" | "claude_pro" | ...), `organizationRateLimitTier`
|
|
12
|
+
* ("default_claude_max_5x" | "default_claude_max_20x" | ...), and
|
|
13
|
+
* `billingType` ("stripe_subscription" | ...).
|
|
14
|
+
* - Codex CLI: `~/.codex/auth.json` -> `auth_mode` ("chatgpt" | "apikey");
|
|
15
|
+
* in chatgpt mode the locally stored id_token's claims carry
|
|
16
|
+
* `chatgpt_plan_type` ("plus" | "pro" | ...).
|
|
17
|
+
*
|
|
18
|
+
* Privacy contract: this module reads ONLY the whitelisted plan/billing
|
|
19
|
+
* fields above. Token values are never read into results, never returned,
|
|
20
|
+
* never logged. Everything is local; no network calls.
|
|
21
|
+
*/
|
|
22
|
+
export type DetectedPlan = {
|
|
23
|
+
agent: "claude-code" | "codex";
|
|
24
|
+
provider: "anthropic" | "openai";
|
|
25
|
+
/** Matches a `subscriptionPlans` id when the plan is one we can price. */
|
|
26
|
+
planId?: string;
|
|
27
|
+
/** Human-readable plan label; falls back to the raw local identifier. */
|
|
28
|
+
planLabel: string;
|
|
29
|
+
billing: "subscription" | "api_key" | "unknown";
|
|
30
|
+
/** Where the detection came from (file or "--plan override"). */
|
|
31
|
+
source: string;
|
|
32
|
+
};
|
|
33
|
+
export type PlanDetectionOptions = {
|
|
34
|
+
/** Default: ~/.claude.json (Claude Code's top-level config). */
|
|
35
|
+
claudeConfigPath?: string;
|
|
36
|
+
/** Default: ~/.codex/auth.json. */
|
|
37
|
+
codexAuthPath?: string;
|
|
38
|
+
};
|
|
39
|
+
/** Detect the plans this machine's coding agents are signed in with. */
|
|
40
|
+
export declare function detectLocalPlans(options?: PlanDetectionOptions): Promise<DetectedPlan[]>;
|
|
41
|
+
//# sourceMappingURL=planDetection.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/** Detect the plans this machine's coding agents are signed in with. */
|
|
5
|
+
export async function detectLocalPlans(options = {}) {
|
|
6
|
+
const home = homedir();
|
|
7
|
+
const claudeConfigPath = options.claudeConfigPath ?? join(home, ".claude.json");
|
|
8
|
+
const codexAuthPath = options.codexAuthPath ?? join(home, ".codex", "auth.json");
|
|
9
|
+
const plans = [];
|
|
10
|
+
const claude = await detectClaudePlan(claudeConfigPath);
|
|
11
|
+
if (claude)
|
|
12
|
+
plans.push(claude);
|
|
13
|
+
const codex = await detectCodexPlan(codexAuthPath);
|
|
14
|
+
if (codex)
|
|
15
|
+
plans.push(codex);
|
|
16
|
+
return plans;
|
|
17
|
+
}
|
|
18
|
+
async function detectClaudePlan(configPath) {
|
|
19
|
+
const config = await readJsonQuietly(configPath);
|
|
20
|
+
const account = isRecord(config) && isRecord(config.oauthAccount) ? config.oauthAccount : undefined;
|
|
21
|
+
if (!account)
|
|
22
|
+
return undefined;
|
|
23
|
+
const organizationType = stringOf(account.organizationType);
|
|
24
|
+
const rateLimitTier = stringOf(account.organizationRateLimitTier) ?? stringOf(account.userRateLimitTier);
|
|
25
|
+
const billingType = stringOf(account.billingType);
|
|
26
|
+
if (!organizationType && !rateLimitTier)
|
|
27
|
+
return undefined;
|
|
28
|
+
let planId;
|
|
29
|
+
let planLabel;
|
|
30
|
+
if (rateLimitTier && /max_20x/i.test(rateLimitTier)) {
|
|
31
|
+
planId = "claude-max-20x";
|
|
32
|
+
planLabel = "Claude Max 20x";
|
|
33
|
+
}
|
|
34
|
+
else if (rateLimitTier && /max_5x/i.test(rateLimitTier)) {
|
|
35
|
+
planId = "claude-max-5x";
|
|
36
|
+
planLabel = "Claude Max 5x";
|
|
37
|
+
}
|
|
38
|
+
else if (organizationType === "claude_pro" || (rateLimitTier && /pro/i.test(rateLimitTier))) {
|
|
39
|
+
planId = "claude-pro";
|
|
40
|
+
planLabel = "Claude Pro";
|
|
41
|
+
}
|
|
42
|
+
else if (organizationType === "claude_max") {
|
|
43
|
+
// Max org but an unrecognized tier string: say what we know, price nothing.
|
|
44
|
+
planLabel = `Claude Max (tier: ${rateLimitTier ?? "unknown"})`;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
planLabel = organizationType ?? rateLimitTier ?? "unknown";
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
agent: "claude-code",
|
|
51
|
+
provider: "anthropic",
|
|
52
|
+
planId,
|
|
53
|
+
planLabel,
|
|
54
|
+
billing: billingType === "stripe_subscription" ? "subscription" : billingType ? "unknown" : "unknown",
|
|
55
|
+
source: configPath
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function detectCodexPlan(authPath) {
|
|
59
|
+
const auth = await readJsonQuietly(authPath);
|
|
60
|
+
if (!isRecord(auth))
|
|
61
|
+
return undefined;
|
|
62
|
+
const authMode = stringOf(auth.auth_mode);
|
|
63
|
+
if (authMode === "apikey") {
|
|
64
|
+
return {
|
|
65
|
+
agent: "codex",
|
|
66
|
+
provider: "openai",
|
|
67
|
+
planLabel: "API key (pay per token)",
|
|
68
|
+
billing: "api_key",
|
|
69
|
+
source: authPath
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const tokens = isRecord(auth.tokens) ? auth.tokens : undefined;
|
|
73
|
+
const idToken = tokens ? stringOf(tokens.id_token) : undefined;
|
|
74
|
+
const claims = idToken ? decodeJwtClaims(idToken) : undefined;
|
|
75
|
+
const authClaims = claims && isRecord(claims["https://api.openai.com/auth"])
|
|
76
|
+
? claims["https://api.openai.com/auth"]
|
|
77
|
+
: undefined;
|
|
78
|
+
const planType = authClaims ? stringOf(authClaims.chatgpt_plan_type) : undefined;
|
|
79
|
+
if (!planType && authMode !== "chatgpt")
|
|
80
|
+
return undefined;
|
|
81
|
+
let planId;
|
|
82
|
+
let planLabel;
|
|
83
|
+
if (planType === "plus") {
|
|
84
|
+
planId = "chatgpt-plus";
|
|
85
|
+
planLabel = "ChatGPT Plus";
|
|
86
|
+
}
|
|
87
|
+
else if (planType === "pro") {
|
|
88
|
+
planId = "chatgpt-pro";
|
|
89
|
+
planLabel = "ChatGPT Pro";
|
|
90
|
+
}
|
|
91
|
+
else if (planType) {
|
|
92
|
+
// e.g. "team", "prolite": name it honestly, don't guess a price.
|
|
93
|
+
planLabel = `ChatGPT (plan: ${planType})`;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
planLabel = "ChatGPT (plan unknown)";
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
agent: "codex",
|
|
100
|
+
provider: "openai",
|
|
101
|
+
planId,
|
|
102
|
+
planLabel,
|
|
103
|
+
billing: "subscription",
|
|
104
|
+
source: authPath
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Decode a JWT's claims segment locally (base64url JSON). No verification —
|
|
109
|
+
* we are reading our own user's already-trusted local file for display
|
|
110
|
+
* metadata, not authenticating anything.
|
|
111
|
+
*/
|
|
112
|
+
function decodeJwtClaims(jwt) {
|
|
113
|
+
const segment = jwt.split(".")[1];
|
|
114
|
+
if (!segment)
|
|
115
|
+
return undefined;
|
|
116
|
+
try {
|
|
117
|
+
const json = Buffer.from(segment, "base64url").toString("utf8");
|
|
118
|
+
const parsed = JSON.parse(json);
|
|
119
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function readJsonQuietly(path) {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function stringOf(value) {
|
|
134
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
135
|
+
}
|
|
136
|
+
function isRecord(value) {
|
|
137
|
+
return typeof value === "object" && value !== null;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=planDetection.js.map
|
package/dist/planMath.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DetectedPlan } from "./planDetection.js";
|
|
1
2
|
import type { UsageRecord } from "./schema.js";
|
|
2
3
|
/**
|
|
3
4
|
* Plan-price math: compares API-equivalent usage (from local agent logs)
|
|
@@ -36,6 +37,10 @@ export type PlanCheck = {
|
|
|
36
37
|
* money's worth? Present only when a plan covers the usage.
|
|
37
38
|
*/
|
|
38
39
|
valueMultiple?: number;
|
|
40
|
+
/** The plan actually detected on this machine (or --plan override), if any. */
|
|
41
|
+
detectedPlan?: DetectedPlan;
|
|
42
|
+
/** Set when projected usage exceeds what the detected tier typically covers. */
|
|
43
|
+
upgradeHint?: string;
|
|
39
44
|
/** One-line, render-ready verdict. */
|
|
40
45
|
headline: string;
|
|
41
46
|
};
|
|
@@ -43,6 +48,11 @@ export type PlanCheck = {
|
|
|
43
48
|
* Compute per-agent plan checks from usage records. Only records that came
|
|
44
49
|
* from local agent logs participate (billing-API records already have real
|
|
45
50
|
* prices and a real plan behind them).
|
|
51
|
+
*
|
|
52
|
+
* When `detectedPlans` carries a locally detected plan (or --plan override)
|
|
53
|
+
* for an agent, the check speaks in facts ("you're on Claude Max 5x") instead
|
|
54
|
+
* of guesses ("Max 20x likely covers this") — and warns when projected usage
|
|
55
|
+
* exceeds what the detected tier typically covers.
|
|
46
56
|
*/
|
|
47
|
-
export declare function computePlanChecks(records: UsageRecord[]): PlanCheck[];
|
|
57
|
+
export declare function computePlanChecks(records: UsageRecord[], detectedPlans?: DetectedPlan[]): PlanCheck[];
|
|
48
58
|
//# sourceMappingURL=planMath.d.ts.map
|
package/dist/planMath.js
CHANGED
|
@@ -10,8 +10,13 @@ const localLogCostType = "local_agent_logs";
|
|
|
10
10
|
* Compute per-agent plan checks from usage records. Only records that came
|
|
11
11
|
* from local agent logs participate (billing-API records already have real
|
|
12
12
|
* prices and a real plan behind them).
|
|
13
|
+
*
|
|
14
|
+
* When `detectedPlans` carries a locally detected plan (or --plan override)
|
|
15
|
+
* for an agent, the check speaks in facts ("you're on Claude Max 5x") instead
|
|
16
|
+
* of guesses ("Max 20x likely covers this") — and warns when projected usage
|
|
17
|
+
* exceeds what the detected tier typically covers.
|
|
13
18
|
*/
|
|
14
|
-
export function computePlanChecks(records) {
|
|
19
|
+
export function computePlanChecks(records, detectedPlans = []) {
|
|
15
20
|
const localRecords = records.filter((record) => record.providerCostType === localLogCostType &&
|
|
16
21
|
(record.agentId === "claude-code" || record.agentId === "codex") &&
|
|
17
22
|
typeof record.amountUsd === "number");
|
|
@@ -35,25 +40,61 @@ export function computePlanChecks(records) {
|
|
|
35
40
|
// (days with usage), which can differ from the calendar window shown
|
|
36
41
|
// elsewhere on the readout — a technical reader will divide and check.
|
|
37
42
|
const basis = `projected from ${windowDays} active day${windowDays === 1 ? "" : "s"}`;
|
|
38
|
-
const
|
|
39
|
-
const
|
|
43
|
+
const detected = detectedPlans.find((plan) => plan.agent === agent);
|
|
44
|
+
const detectedKnown = detected?.planId
|
|
45
|
+
? subscriptionPlans.find((plan) => plan.id === detected.planId)
|
|
46
|
+
: undefined;
|
|
40
47
|
let headline;
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
let valueMultiple;
|
|
49
|
+
let upgradeHint;
|
|
50
|
+
let effectiveSavings;
|
|
51
|
+
if (detectedKnown) {
|
|
52
|
+
// FACT mode: we know the user's actual plan from local agent config.
|
|
53
|
+
valueMultiple = Math.round((monthly / detectedKnown.monthlyUsd) * 10) / 10;
|
|
54
|
+
const savingsVsApi = roundMoney(monthly - detectedKnown.monthlyUsd);
|
|
55
|
+
effectiveSavings = savingsVsApi > 0 ? savingsVsApi : undefined;
|
|
56
|
+
headline =
|
|
57
|
+
`${agent}: ~$${monthly.toFixed(2)}/mo at API rates (${basis}) — you're on ${detectedKnown.name} ` +
|
|
58
|
+
`($${detectedKnown.monthlyUsd}/mo, detected locally): ~${valueMultiple}× the plan price in usage` +
|
|
59
|
+
(effectiveSavings ? `, ~$${effectiveSavings.toFixed(2)}/mo cheaper than paying per token.` : `.`);
|
|
60
|
+
if (monthly > detectedKnown.coversUpToUsd) {
|
|
61
|
+
const nextTier = subscriptionPlans.find((plan) => plan.agent === agent && plan.coversUpToUsd > detectedKnown.coversUpToUsd);
|
|
62
|
+
upgradeHint = nextTier
|
|
63
|
+
? `usage runs past what ${detectedKnown.name} typically covers (~$${detectedKnown.coversUpToUsd}/mo) — if you're hitting rate limits, ${nextTier.name} ($${nextTier.monthlyUsd}/mo) is the next tier; trimming context (below) buys headroom without upgrading.`
|
|
64
|
+
: `usage runs past what ${detectedKnown.name} typically covers (~$${detectedKnown.coversUpToUsd}/mo) — trimming context (below) is the main headroom lever.`;
|
|
65
|
+
}
|
|
43
66
|
}
|
|
44
|
-
else if (
|
|
45
|
-
|
|
67
|
+
else if (detected) {
|
|
68
|
+
// Detected a plan we can't price (e.g. an unrecognized tier): state the
|
|
69
|
+
// fact, then fall back to suggestion math without pretending certainty.
|
|
70
|
+
headline =
|
|
71
|
+
`${agent}: ~$${monthly.toFixed(2)}/mo at API rates (${basis}) — you're on ${detected.planLabel} ` +
|
|
72
|
+
`(detected locally; price not in our table)` +
|
|
73
|
+
(suggested ? `; closest known plan: ${suggested.name} ($${suggested.monthlyUsd}/mo).` : `.`);
|
|
46
74
|
}
|
|
47
75
|
else {
|
|
48
|
-
|
|
76
|
+
const covered = suggested && typeof savings === "number" && savings > 0;
|
|
77
|
+
valueMultiple = covered ? Math.round((monthly / suggested.monthlyUsd) * 10) / 10 : undefined;
|
|
78
|
+
effectiveSavings = covered ? savings : undefined;
|
|
79
|
+
if (!suggested) {
|
|
80
|
+
headline = `${agent}: ~$${monthly.toFixed(2)}/mo at API rates (${basis}).`;
|
|
81
|
+
}
|
|
82
|
+
else if (covered) {
|
|
83
|
+
headline = `${agent}: ~$${monthly.toFixed(2)}/mo at API rates (${basis}) — ${suggested.name} ($${suggested.monthlyUsd}/mo) likely covers this. You're getting ~${valueMultiple}× the plan price in usage, ~$${savings.toFixed(2)}/mo cheaper than paying per token.`;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
headline = `${agent}: ~$${monthly.toFixed(2)}/mo at API rates (${basis}) — within ${suggested.name} ($${suggested.monthlyUsd}/mo); pay-as-you-go API could be cheaper if you drop the subscription.`;
|
|
87
|
+
}
|
|
49
88
|
}
|
|
50
89
|
checks.push({
|
|
51
90
|
agent,
|
|
52
91
|
apiEquivalentMonthlyUsd: monthly,
|
|
53
92
|
windowDays,
|
|
54
|
-
suggestedPlan: suggested,
|
|
55
|
-
monthlySavingsVsApiUsd:
|
|
93
|
+
suggestedPlan: detectedKnown ?? suggested,
|
|
94
|
+
monthlySavingsVsApiUsd: effectiveSavings,
|
|
56
95
|
valueMultiple,
|
|
96
|
+
detectedPlan: detected,
|
|
97
|
+
upgradeHint,
|
|
57
98
|
headline
|
|
58
99
|
});
|
|
59
100
|
}
|