@rosh100yx/outlier 0.4.24 → 0.7.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 +39 -4
- package/bin/outlier.js +529 -123
- package/bin/postinstall.js +18 -17
- package/data/grid-factors.json +16 -3
- package/package.json +1 -1
- package/src/capabilities.ts +98 -58
- package/src/carbon.ts +80 -20
- package/src/cli.ts +181 -34
- package/src/emissions.ts +69 -0
- package/src/sources.ts +110 -0
package/bin/postinstall.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const cyan = (
|
|
4
|
-
const dim = (
|
|
5
|
-
const bold = (
|
|
3
|
+
const cyan = (t) => `\x1b[36m${t}\x1b[0m`;
|
|
4
|
+
const dim = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
5
|
+
const bold = (t) => `\x1b[1m${t}\x1b[0m`;
|
|
6
|
+
const green = (t) => `\x1b[32m${t}\x1b[0m`;
|
|
6
7
|
|
|
7
|
-
console.log('\n' + bold('
|
|
8
|
-
console.log(dim('
|
|
9
|
-
console.log('
|
|
10
|
-
console.log(
|
|
11
|
-
console.log('
|
|
12
|
-
console.log(
|
|
13
|
-
console.log(
|
|
14
|
-
console.log(
|
|
15
|
-
console.log(
|
|
16
|
-
console.log(`
|
|
17
|
-
console.log(
|
|
18
|
-
console.log(
|
|
19
|
-
console.log(
|
|
20
|
-
console.log(
|
|
8
|
+
console.log('\n' + bold(' Outlier installed') + dim(' · AI code governance for the terminal'));
|
|
9
|
+
console.log(dim(' ──────────────────────────────────────────────────────────'));
|
|
10
|
+
console.log(' Run it before you start coding. It reads your local git history');
|
|
11
|
+
console.log(' and AI logs — ' + green('on your machine') + ' — and shows you:');
|
|
12
|
+
console.log(' • how much of your code AI wrote');
|
|
13
|
+
console.log(' • what it cost (tokens, $, wasted context, carbon)');
|
|
14
|
+
console.log(' • whether you are keeping the skill while you use the speed');
|
|
15
|
+
console.log('');
|
|
16
|
+
console.log(' Start your first audit:');
|
|
17
|
+
console.log(` ${cyan('outlier')}`);
|
|
18
|
+
console.log('');
|
|
19
|
+
console.log(' Other commands: ' + dim('outlier --help'));
|
|
20
|
+
console.log(dim(' ──────────────────────────────────────────────────────────'));
|
|
21
|
+
console.log(' ' + green('Local-first:') + ' nothing ever leaves your machine.\n');
|
package/data/grid-factors.json
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"vietnam": 681,
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"india_average": 715,
|
|
4
|
+
"indonesia": 650,
|
|
5
|
+
"china": 581,
|
|
5
6
|
"singapore": 408,
|
|
6
|
-
"
|
|
7
|
+
"japan": 470,
|
|
8
|
+
"south_korea": 415,
|
|
9
|
+
"australia": 510,
|
|
10
|
+
"us_east": 380,
|
|
11
|
+
"us_west": 210,
|
|
12
|
+
"canada": 120,
|
|
13
|
+
"brazil": 95,
|
|
14
|
+
"uk": 210,
|
|
15
|
+
"germany": 350,
|
|
16
|
+
"france": 21.7,
|
|
17
|
+
"norway": 28,
|
|
18
|
+
"sweden": 41,
|
|
19
|
+
"global_average": 450
|
|
7
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rosh100yx/outlier",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "AI Code Governance & Capability Auditing for the Terminal. Measures AI reliance, context waste, and enforces local CI/CD policies.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"outlier": "bin/outlier.js"
|
package/src/capabilities.ts
CHANGED
|
@@ -1,76 +1,116 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { existsSync, readdirSync } from 'fs';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
// Reach = what a tool can actually do to you if an agent (or a prompt injection) drives it.
|
|
6
|
+
export type Reach = 'read' | 'network' | 'model' | 'data' | 'write-local' | 'write-remote' | 'deploy' | 'exec' | 'money';
|
|
7
|
+
|
|
8
|
+
export interface ToolReach {
|
|
9
|
+
name: string;
|
|
10
|
+
reach: Reach;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type BlastRadius = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
|
4
14
|
|
|
5
15
|
export interface CapabilitiesStats {
|
|
6
|
-
mcps:
|
|
16
|
+
mcps: ToolReach[];
|
|
7
17
|
skills: string[];
|
|
18
|
+
subagents: number;
|
|
19
|
+
hooks: string[]; // lifecycle events with auto-firing hooks
|
|
8
20
|
hasOrchestration: boolean;
|
|
21
|
+
blastRadius: BlastRadius;
|
|
22
|
+
blastReasons: string[]; // plain-language reasons for the score
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Classify an MCP/tool by name into a reach category. Keyword-based, since names vary.
|
|
26
|
+
export function classifyReach(name: string): Reach {
|
|
27
|
+
const n = name.toLowerCase();
|
|
28
|
+
if (/(stripe|payment|infinity|kucoin|wallet|billing|payout)/.test(n)) return 'money';
|
|
29
|
+
if (/(shell|bash|exec|terminal|command|sandbox)/.test(n)) return 'exec';
|
|
30
|
+
if (/(cloudflare|vercel|netlify|modal|deploy|fly\.io|render|heroku|aws|gcp|azure)/.test(n)) return 'deploy';
|
|
31
|
+
if (/(github|gitlab|git|bitbucket)/.test(n)) return 'write-remote';
|
|
32
|
+
if (/(filesystem|file|fs|disk)/.test(n)) return 'write-local';
|
|
33
|
+
if (/(memory|supermemory|mem|obsidian|notion|airtable|coda|database|sql|store)/.test(n)) return 'data';
|
|
34
|
+
if (/(openrouter|ollama|openai|anthropic|llm|model|hugging)/.test(n)) return 'model';
|
|
35
|
+
if (/(exa|web|fetch|search|brave|browser|http|scrape)/.test(n)) return 'network';
|
|
36
|
+
return 'network'; // unknown tool: assume it can reach out
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const REACH_RISK: Record<Reach, number> = {
|
|
40
|
+
read: 0, model: 1, network: 1, 'data': 2, 'write-local': 2, 'write-remote': 3, deploy: 3, exec: 4, money: 4,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function scoreBlast(reaches: Reach[]): { radius: BlastRadius; reasons: string[] } {
|
|
44
|
+
const reasons: string[] = [];
|
|
45
|
+
const has = (r: Reach) => reaches.includes(r);
|
|
46
|
+
if (has('money')) reasons.push('can move money');
|
|
47
|
+
if (has('exec')) reasons.push('can run shell commands');
|
|
48
|
+
if (has('deploy')) reasons.push('can deploy to production');
|
|
49
|
+
if (has('write-remote')) reasons.push('can push to your remote repos');
|
|
50
|
+
if (has('write-local')) reasons.push('can write your local files');
|
|
51
|
+
if (has('data')) reasons.push('can read/write your stored data');
|
|
52
|
+
const netCount = reaches.filter(r => r === 'network' || r === 'model').length;
|
|
53
|
+
if (netCount >= 3) reasons.push(`reaches ${netCount} external services`);
|
|
54
|
+
|
|
55
|
+
const max = reaches.reduce((m, r) => Math.max(m, REACH_RISK[r]), 0);
|
|
56
|
+
let radius: BlastRadius = 'LOW';
|
|
57
|
+
if (max >= 4) radius = 'CRITICAL';
|
|
58
|
+
else if (max >= 3) radius = 'HIGH';
|
|
59
|
+
else if (max >= 2) radius = 'MEDIUM';
|
|
60
|
+
return { radius, reasons };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readJson(path: string): any {
|
|
64
|
+
try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return null; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function countDir(path: string, ext = '.md'): number {
|
|
68
|
+
try { return readdirSync(path).filter(f => f.endsWith(ext)).length; } catch { return 0; }
|
|
9
69
|
}
|
|
10
70
|
|
|
11
71
|
export async function getCapabilitiesStats(repoPath: string = process.cwd(), homeDirPath: string = homedir()): Promise<CapabilitiesStats> {
|
|
12
|
-
const
|
|
13
|
-
mcps: [],
|
|
14
|
-
skills: [],
|
|
15
|
-
hasOrchestration: false
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// Check for AGENTS.md (Orchestration Policy)
|
|
19
|
-
if (existsSync(join(repoPath, 'AGENTS.md'))) {
|
|
20
|
-
stats.hasOrchestration = true;
|
|
21
|
-
}
|
|
72
|
+
const mcpNames = new Set<string>();
|
|
22
73
|
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
74
|
+
// MCP servers — the agent's actual tool reach. Read from every place they live.
|
|
75
|
+
for (const cfg of [
|
|
76
|
+
join(homeDirPath, '.claude.json'),
|
|
77
|
+
join(homeDirPath, '.claude', 'settings.json'),
|
|
78
|
+
join(repoPath, '.mcp.json'),
|
|
79
|
+
join(repoPath, '.claude', 'settings.json'),
|
|
80
|
+
]) {
|
|
81
|
+
const j = readJson(cfg);
|
|
82
|
+
if (j?.mcpServers) Object.keys(j.mcpServers).forEach(k => mcpNames.add(k));
|
|
32
83
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const skills = readdirSync(geminiSkillsPath, { withFileTypes: true })
|
|
39
|
-
.filter(dirent => dirent.isDirectory())
|
|
40
|
-
.map(dirent => dirent.name);
|
|
41
|
-
|
|
42
|
-
// Only add if not already added (dedupe)
|
|
43
|
-
for (const skill of skills) {
|
|
44
|
-
if (!stats.skills.includes(skill)) stats.skills.push(skill);
|
|
45
|
-
}
|
|
46
|
-
} catch (e) {}
|
|
84
|
+
// Gemini-style MCP dir
|
|
85
|
+
const geminiMcp = join(homeDirPath, '.gemini', 'antigravity-cli', 'mcp');
|
|
86
|
+
if (existsSync(geminiMcp)) {
|
|
87
|
+
try { readdirSync(geminiMcp, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => mcpNames.add(d.name)); } catch {}
|
|
47
88
|
}
|
|
48
89
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
} catch (e) {}
|
|
90
|
+
const mcps: ToolReach[] = [...mcpNames].map(name => ({ name, reach: classifyReach(name) }));
|
|
91
|
+
|
|
92
|
+
// Skills
|
|
93
|
+
const skills: string[] = [];
|
|
94
|
+
for (const p of [join(repoPath, '.agents', 'skills'), join(homeDirPath, '.claude', 'skills'), join(homeDirPath, '.gemini', 'skills')]) {
|
|
95
|
+
if (existsSync(p)) {
|
|
96
|
+
try { readdirSync(p, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => { if (!skills.includes(d.name)) skills.push(d.name); }); } catch {}
|
|
97
|
+
}
|
|
58
98
|
}
|
|
59
99
|
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
stats.mcps.push(`plugin:${plugin}`);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
} catch (e) {}
|
|
100
|
+
// Sub-agents (Claude Code agent definitions)
|
|
101
|
+
const subagents = countDir(join(homeDirPath, '.claude', 'agents')) + countDir(join(repoPath, '.claude', 'agents'));
|
|
102
|
+
|
|
103
|
+
// Hooks — automation that fires on the developer's behalf (a real governance surface)
|
|
104
|
+
const hooks: string[] = [];
|
|
105
|
+
for (const cfg of [join(homeDirPath, '.claude', 'settings.json'), join(repoPath, '.claude', 'settings.json')]) {
|
|
106
|
+
const j = readJson(cfg);
|
|
107
|
+
if (j?.hooks) Object.keys(j.hooks).forEach(k => { if (!hooks.includes(k)) hooks.push(k); });
|
|
73
108
|
}
|
|
74
109
|
|
|
75
|
-
|
|
110
|
+
// Orchestration policy
|
|
111
|
+
const hasOrchestration = existsSync(join(repoPath, 'AGENTS.md')) || existsSync(join(repoPath, '.mcp.json'));
|
|
112
|
+
|
|
113
|
+
const { radius, reasons } = scoreBlast(mcps.map(m => m.reach));
|
|
114
|
+
|
|
115
|
+
return { mcps, skills, subagents, hooks, hasOrchestration, blastRadius: radius, blastReasons: reasons };
|
|
76
116
|
}
|
package/src/carbon.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { readFile, access } from 'fs/promises';
|
|
3
|
+
import { readFile, access, readdir } from 'fs/promises';
|
|
4
4
|
import gridFactors from '../data/grid-factors.json';
|
|
5
|
+
import { energyKwhByModel } from './emissions';
|
|
6
|
+
import { detectSources, provLabel, type Provenance } from './sources';
|
|
5
7
|
|
|
6
8
|
export interface CarbonStats {
|
|
7
9
|
totalTokens: number;
|
|
@@ -13,51 +15,98 @@ export interface CarbonStats {
|
|
|
13
15
|
localCo2Kg: number;
|
|
14
16
|
localRegion: string;
|
|
15
17
|
sessions: number;
|
|
16
|
-
estUsd: number;
|
|
17
|
-
costIsReal: boolean;
|
|
18
|
+
estUsd: number; // estimated spend in USD
|
|
19
|
+
costIsReal: boolean; // true if summed from the log's own cost field, false if estimated
|
|
20
|
+
tokenProvenance: Provenance;
|
|
21
|
+
carbonProvenance: Provenance;
|
|
22
|
+
sourceLabel: string; // e.g. "estimated · Claude Code transcripts"
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export interface TokenLogParser {
|
|
21
|
-
parse(): Promise<{ total: number, output: number, cache: number, sessions: number, cost: number }>;
|
|
26
|
+
parse(): Promise<{ total: number, output: number, cache: number, sessions: number, cost: number, outputByModel: Record<string, number> }>;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export class ClaudeLogParser implements TokenLogParser {
|
|
25
30
|
private baseDir: string;
|
|
26
|
-
|
|
31
|
+
private cwd: string;
|
|
32
|
+
constructor(baseDir = homedir(), cwd = process.cwd()) {
|
|
27
33
|
this.baseDir = baseDir;
|
|
34
|
+
this.cwd = cwd;
|
|
28
35
|
}
|
|
36
|
+
|
|
29
37
|
async parse() {
|
|
38
|
+
// Primary source: the standard Claude Code session transcripts for THIS repo.
|
|
39
|
+
// Claude Code stores them at ~/.claude/projects/<cwd-with-slashes-as-dashes>/*.jsonl,
|
|
40
|
+
// one JSON object per line; assistant turns carry `message.usage`.
|
|
41
|
+
const slug = this.cwd.replace(/\//g, '-');
|
|
42
|
+
const projectDir = join(this.baseDir, '.claude', 'projects', slug);
|
|
43
|
+
try {
|
|
44
|
+
const files = (await readdir(projectDir)).filter(f => f.endsWith('.jsonl'));
|
|
45
|
+
if (files.length > 0) {
|
|
46
|
+
let total = 0, output = 0, cache = 0;
|
|
47
|
+
const sessions = new Set<string>();
|
|
48
|
+
const outputByModel: Record<string, number> = {};
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
let text = '';
|
|
51
|
+
try { text = await readFile(join(projectDir, file), 'utf-8'); } catch { continue; }
|
|
52
|
+
for (const line of text.split('\n')) {
|
|
53
|
+
if (!line.trim()) continue;
|
|
54
|
+
try {
|
|
55
|
+
const d = JSON.parse(line);
|
|
56
|
+
const msg = d.message || {};
|
|
57
|
+
const u = msg.usage || d.usage;
|
|
58
|
+
if (u) {
|
|
59
|
+
const inp = u.input_tokens || 0;
|
|
60
|
+
const out = u.output_tokens || 0;
|
|
61
|
+
const cr = u.cache_read_input_tokens || 0;
|
|
62
|
+
const cw = u.cache_creation_input_tokens || 0;
|
|
63
|
+
total += inp + out + cr + cw;
|
|
64
|
+
output += out;
|
|
65
|
+
cache += cr;
|
|
66
|
+
const model = msg.model || 'default';
|
|
67
|
+
outputByModel[model] = (outputByModel[model] || 0) + out;
|
|
68
|
+
}
|
|
69
|
+
if (d.sessionId) sessions.add(d.sessionId);
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Standard transcripts carry no cost field; cost is estimated downstream.
|
|
74
|
+
return { total, output, cache, sessions: sessions.size, cost: 0, outputByModel };
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
|
|
78
|
+
// Fallback: the optional tokenomics-log.jsonl (written by a custom Stop hook;
|
|
79
|
+
// carries a real cost_usd field when present).
|
|
30
80
|
const logPath = join(this.baseDir, '.claude', 'tokenomics-log.jsonl');
|
|
31
|
-
|
|
32
81
|
try {
|
|
33
82
|
await access(logPath);
|
|
34
83
|
} catch {
|
|
35
|
-
return { total: 0, output: 0, cache: 0, sessions: 0, cost: 0 };
|
|
84
|
+
return { total: 0, output: 0, cache: 0, sessions: 0, cost: 0, outputByModel: {} };
|
|
36
85
|
}
|
|
37
|
-
|
|
38
86
|
const text = await readFile(logPath, 'utf-8');
|
|
39
|
-
const lines = text.trim().split('\n').filter(l => l.length > 0);
|
|
40
|
-
|
|
41
87
|
let total = 0, output = 0, cache = 0, cost = 0;
|
|
42
88
|
const sessions = new Set<string>();
|
|
43
|
-
|
|
44
|
-
for (const line of
|
|
89
|
+
const outputByModel: Record<string, number> = {};
|
|
90
|
+
for (const line of text.split('\n')) {
|
|
91
|
+
if (!line.trim()) continue;
|
|
45
92
|
try {
|
|
46
93
|
const data = JSON.parse(line);
|
|
47
94
|
total += data.total_tokens || 0;
|
|
48
95
|
output += data.output_tokens || 0;
|
|
49
96
|
cache += data.cache_read || 0;
|
|
50
|
-
cost += data.cost_usd || 0;
|
|
97
|
+
cost += data.cost_usd || 0;
|
|
51
98
|
if (data.session_id) sessions.add(data.session_id);
|
|
52
|
-
|
|
99
|
+
const model = data.model || 'default';
|
|
100
|
+
outputByModel[model] = (outputByModel[model] || 0) + (data.output_tokens || 0);
|
|
101
|
+
} catch {}
|
|
53
102
|
}
|
|
54
|
-
return { total, output, cache, sessions: sessions.size, cost };
|
|
103
|
+
return { total, output, cache, sessions: sessions.size, cost, outputByModel };
|
|
55
104
|
}
|
|
56
105
|
}
|
|
57
106
|
|
|
58
107
|
class CursorLogParser implements TokenLogParser {
|
|
59
108
|
async parse() {
|
|
60
|
-
return { total: 0, output: 0, cache: 0, sessions: 0, cost: 0 };
|
|
109
|
+
return { total: 0, output: 0, cache: 0, sessions: 0, cost: 0, outputByModel: {} };
|
|
61
110
|
}
|
|
62
111
|
}
|
|
63
112
|
|
|
@@ -77,13 +126,14 @@ function getLocalGridFactor(): { region: string, factor: number } {
|
|
|
77
126
|
if (tz.includes('Singapore')) return { region: 'Singapore', factor: gridFactors.singapore };
|
|
78
127
|
if (tz.includes('Calcutta') || tz.includes('Kolkata') || tz.includes('Asia/Kabul')) return { region: 'India', factor: gridFactors.india_average };
|
|
79
128
|
} catch (e) {}
|
|
80
|
-
return { region: 'Global Average', factor:
|
|
129
|
+
return { region: 'Global Average', factor: gridFactors.global_average };
|
|
81
130
|
}
|
|
82
131
|
|
|
83
132
|
export async function getCarbonStats(): Promise<CarbonStats> {
|
|
84
133
|
const parsers: TokenLogParser[] = [new ClaudeLogParser(), new CursorLogParser()];
|
|
85
|
-
|
|
134
|
+
|
|
86
135
|
let totalTokens = 0, outputTokens = 0, cacheReadTokens = 0, sessions = 0, loggedCost = 0;
|
|
136
|
+
const outputByModel: Record<string, number> = {};
|
|
87
137
|
|
|
88
138
|
for (const parser of parsers) {
|
|
89
139
|
const stats = await parser.parse();
|
|
@@ -92,11 +142,18 @@ export async function getCarbonStats(): Promise<CarbonStats> {
|
|
|
92
142
|
cacheReadTokens += stats.cache;
|
|
93
143
|
sessions += stats.sessions;
|
|
94
144
|
loggedCost += stats.cost;
|
|
145
|
+
for (const [m, out] of Object.entries(stats.outputByModel)) {
|
|
146
|
+
outputByModel[m] = (outputByModel[m] || 0) + out;
|
|
147
|
+
}
|
|
95
148
|
}
|
|
96
149
|
|
|
97
|
-
|
|
150
|
+
// Model-aware energy (replaces the single flat 0.662 coefficient).
|
|
151
|
+
const energyKwh = energyKwhByModel(outputByModel);
|
|
98
152
|
const localGrid = getLocalGridFactor();
|
|
99
153
|
|
|
154
|
+
// Source provenance for honest labelling in the UI.
|
|
155
|
+
const sources = detectSources();
|
|
156
|
+
|
|
100
157
|
// Prefer the log's own cost field (accurate); fall back to a rough token estimate.
|
|
101
158
|
const costIsReal = loggedCost > 0;
|
|
102
159
|
const estUsd = costIsReal ? loggedCost : estimateUsd(outputTokens, cacheReadTokens, totalTokens);
|
|
@@ -112,6 +169,9 @@ export async function getCarbonStats(): Promise<CarbonStats> {
|
|
|
112
169
|
localRegion: localGrid.region,
|
|
113
170
|
sessions,
|
|
114
171
|
estUsd,
|
|
115
|
-
costIsReal
|
|
172
|
+
costIsReal,
|
|
173
|
+
tokenProvenance: sources.tokenSource.provenance,
|
|
174
|
+
carbonProvenance: sources.carbonSource.provenance,
|
|
175
|
+
sourceLabel: provLabel(sources.tokenSource)
|
|
116
176
|
};
|
|
117
177
|
}
|