Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 163 additions & 6 deletions README.md

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions packages/cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
} from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { Database } from 'bun:sqlite';

const REGISTRY_FIXTURES_DIR = join(import.meta.dir, '..', '..', 'registry', 'src', '__fixtures__');

Expand Down Expand Up @@ -760,6 +761,44 @@ describe('run', () => {
cleanup();
}
});

test('resolveTabbedDashboardProviders keeps synthetic selection scoped', async () => {
const root = mkdtempSync(join(tmpdir(), 'tokenleak-synthetic-cli-'));
const dbPath = join(root, 'sqlite.db');
const db = new Database(dbPath);
db.run('CREATE TABLE messages (id TEXT, model TEXT, input_tokens INTEGER, output_tokens INTEGER, timestamp INTEGER, session_id TEXT)');
db.run("INSERT INTO messages VALUES ('m1', 'hf:org/model', 1, 1, 1770724800000, 's1')");
db.close();
const previousEnv = process.env;

try {
process.env = { ...process.env, TOKENLEAK_SYNTHETIC_DIR: dbPath };
const providers = await resolveTabbedDashboardProviders({ providerNames: ['synthetic'] });
expect(providers.map((provider) => provider.name)).toEqual(['synthetic']);
} finally {
process.env = previousEnv;
rmSync(root, { recursive: true, force: true });
}
});

test('resolveTabbedDashboardProviders excludes synthetic from default provider scans', async () => {
const root = mkdtempSync(join(tmpdir(), 'tokenleak-synthetic-default-cli-'));
const dbPath = join(root, 'sqlite.db');
const db = new Database(dbPath);
db.run('CREATE TABLE messages (id TEXT, model TEXT, input_tokens INTEGER, output_tokens INTEGER, timestamp INTEGER, session_id TEXT)');
db.run("INSERT INTO messages VALUES ('m1', 'hf:org/model', 1, 1, 1770724800000, 's1')");
db.close();
const previousEnv = process.env;

try {
process.env = { ...process.env, TOKENLEAK_SYNTHETIC_DIR: dbPath };
const providers = await resolveTabbedDashboardProviders({ providerNames: [] });
expect(providers.map((provider) => provider.name)).not.toContain('synthetic');
} finally {
process.env = previousEnv;
rmSync(root, { recursive: true, force: true });
}
});
});

describe('runFocus', () => {
Expand Down Expand Up @@ -985,15 +1024,30 @@ describe('CLI invocation', () => {
expect(stdout).toContain('gemini');
expect(stdout).toContain('copilot');
expect(stdout).toContain('amp');
expect(stdout).toContain('codebuff');
expect(stdout).toContain('droid');
expect(stdout).toContain('qwen');
expect(stdout).toContain('roo-code');
expect(stdout).toContain('kilo-code');
expect(stdout).toContain('kimi');
expect(stdout).toContain('kilo');
expect(stdout).toContain('mux');
expect(stdout).toContain('crush');
expect(stdout).toContain('openclaw');
expect(stdout).toContain('hermes');
expect(stdout).toContain('goose');
expect(stdout).toContain('antigravity');
expect(stdout).toContain('zed');
expect(stdout).toContain('kiro');
expect(stdout).toContain('trae');
expect(stdout).toContain('synthetic');
expect(stdout).toContain('pi');
expect(stdout).toContain('open-code');
expect(stdout).toContain('github-copilot');
expect(stdout).toContain('sourcegraph-amp');
expect(stdout).toContain('manicode');
expect(stdout).toContain('kilo-cli');
expect(stdout).toContain('octofriend');
});

test('--all-providers with provider filter exits with code 1', async () => {
Expand Down
75 changes: 73 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,23 @@ import {
GeminiProvider,
CopilotProvider,
AmpProvider,
CodebuffProvider,
DroidProvider,
QwenProvider,
RooCodeProvider,
KiloCodeProvider,
KimiProvider,
KiloProvider,
MuxProvider,
CrushProvider,
OpenClawProvider,
HermesProvider,
GooseProvider,
AntigravityProvider,
ZedProvider,
KiroProvider,
TraeProvider,
SyntheticProvider,
OpenCodeProvider,
PiProvider,
MODEL_PRICING,
Expand Down Expand Up @@ -140,16 +152,34 @@ const PROVIDER_ALIASES: Record<string, string> = {
'copilot-otel': 'copilot',
amp: 'amp',
'sourcegraph-amp': 'amp',
codebuff: 'codebuff',
manicode: 'codebuff',
droid: 'droid',
factory: 'droid',
qwen: 'qwen',
'roo-code': 'roo-code',
roo: 'roo-code',
roocode: 'roo-code',
'kilo-code': 'kilo-code',
kilo: 'kilo-code',
kilocode: 'kilo-code',
kimi: 'kimi',
'kimi-cli': 'kimi',
kilo: 'kilo',
'kilo-cli': 'kilo',
mux: 'mux',
crush: 'crush',
openclaw: 'openclaw',
'open-claw': 'openclaw',
hermes: 'hermes',
'hermes-agent': 'hermes',
goose: 'goose',
antigravity: 'antigravity',
zed: 'zed',
'zed-agent': 'zed',
kiro: 'kiro',
trae: 'trae',
synthetic: 'synthetic',
octofriend: 'synthetic',
};
const PROVIDER_ALIAS_GROUPS: Record<string, string[]> = {
'claude-code': ['anthropic', 'claude', 'claudecode'],
Expand All @@ -160,10 +190,18 @@ const PROVIDER_ALIAS_GROUPS: Record<string, string[]> = {
gemini: ['google'],
copilot: ['github-copilot', 'copilot-otel'],
amp: ['sourcegraph-amp'],
codebuff: ['manicode'],
droid: ['factory'],
'roo-code': ['roo', 'roocode'],
'kilo-code': ['kilo', 'kilocode'],
'kilo-code': ['kilocode'],
kimi: ['kimi-cli'],
kilo: ['kilo-cli'],
openclaw: ['open-claw'],
hermes: ['hermes-agent'],
zed: ['zed-agent'],
synthetic: ['octofriend'],
};
const EXPLICIT_ONLY_PROVIDERS = new Set(['synthetic']);

interface ProviderFilterConfig {
provider?: string;
Expand Down Expand Up @@ -226,6 +264,10 @@ function providerMatchesFilter(provider: IProvider, requested: Set<string>): boo
return candidates.some((candidate) => requested.has(candidate));
}

function isExplicitOnlyProvider(provider: IProvider): boolean {
return EXPLICIT_ONLY_PROVIDERS.has(provider.name);
}

function buildHelpText(): string {
return [
`tokenleak ${VERSION}`,
Expand Down Expand Up @@ -672,11 +714,23 @@ function registerBuiltInProviders(registry: ProviderRegistry): void {
registry.register(new GeminiProvider());
registry.register(new CopilotProvider());
registry.register(new AmpProvider());
registry.register(new CodebuffProvider());
registry.register(new DroidProvider());
registry.register(new QwenProvider());
registry.register(new RooCodeProvider());
registry.register(new KiloCodeProvider());
registry.register(new KimiProvider());
registry.register(new KiloProvider());
registry.register(new MuxProvider());
registry.register(new CrushProvider());
registry.register(new OpenClawProvider());
registry.register(new HermesProvider());
registry.register(new GooseProvider());
registry.register(new AntigravityProvider());
registry.register(new ZedProvider());
registry.register(new KiroProvider());
registry.register(new TraeProvider());
registry.register(new SyntheticProvider());
Comment thread
ya-nsh marked this conversation as resolved.
registry.register(new PiProvider());
registry.register(new OpenCodeProvider());
}
Expand Down Expand Up @@ -745,6 +799,10 @@ async function selectAvailableProviders(
const registry = createRegistry();
let available = await registry.getAvailable();

if (!requestedProviders.has('synthetic')) {
available = available.filter((provider) => !isExplicitOnlyProvider(provider));
}

if (!config.allProviders && requestedProviders.size > 0) {
if (
config.provider &&
Expand Down Expand Up @@ -1247,6 +1305,19 @@ const PROVIDER_COLORS: Record<string, number> = {
'claude-code': 179, // amber
codex: 71, // green
cursor: 78, // spring green
codebuff: 33,
droid: 208,
kimi: 244,
kilo: 214,
mux: 205,
crush: 196,
goose: 37,
antigravity: 99,
zed: 38,
kiro: 63,
trae: 44,
synthetic: 42,
hermes: 35,
pi: 73, // cyan/teal
'open-code': 68, // indigo/steel blue
};
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp/src/resources/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
} from '@tokenleak/core';
import type { ProviderRegistry } from '@tokenleak/registry';
import { resolveRange } from '../shared/date-range.js';
import { loadProviderData } from '../shared/provider-load.js';
import { getAvailableProvidersForRequest, loadProviderData } from '../shared/provider-load.js';

export async function handleOverview(registry: ProviderRegistry): Promise<string> {
const range = resolveRange({});
const available = await registry.getAvailable();
const available = await getAvailableProvidersForRequest(registry);

const { data, warnings } = await loadProviderData(available, range);

Expand Down
24 changes: 24 additions & 0 deletions packages/mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,23 @@ import {
GeminiProvider,
CopilotProvider,
AmpProvider,
CodebuffProvider,
DroidProvider,
QwenProvider,
RooCodeProvider,
KiloCodeProvider,
KimiProvider,
KiloProvider,
MuxProvider,
CrushProvider,
OpenClawProvider,
HermesProvider,
GooseProvider,
AntigravityProvider,
ZedProvider,
KiroProvider,
TraeProvider,
SyntheticProvider,
PiProvider,
OpenCodeProvider,
} from '@tokenleak/registry';
Expand Down Expand Up @@ -46,11 +58,23 @@ function createDefaultRegistry(): ProviderRegistry {
registry.register(new GeminiProvider());
registry.register(new CopilotProvider());
registry.register(new AmpProvider());
registry.register(new CodebuffProvider());
registry.register(new DroidProvider());
registry.register(new QwenProvider());
registry.register(new RooCodeProvider());
registry.register(new KiloCodeProvider());
registry.register(new KimiProvider());
registry.register(new KiloProvider());
registry.register(new MuxProvider());
registry.register(new CrushProvider());
registry.register(new OpenClawProvider());
registry.register(new HermesProvider());
registry.register(new GooseProvider());
registry.register(new AntigravityProvider());
registry.register(new ZedProvider());
registry.register(new KiroProvider());
registry.register(new TraeProvider());
registry.register(new SyntheticProvider());
registry.register(new PiProvider());
registry.register(new OpenCodeProvider());
return registry;
Expand Down
15 changes: 14 additions & 1 deletion packages/mcp/src/shared/provider-load.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import type { DateRange, ProviderData, ProviderWarning } from '@tokenleak/core';
import type { IProvider } from '@tokenleak/registry';
import type { IProvider, ProviderRegistry } from '@tokenleak/registry';

const EXPLICIT_ONLY_PROVIDERS = new Set(['synthetic']);

export interface LoadedProviderData {
data: ProviderData[];
warnings: ProviderWarning[];
}

export async function getAvailableProvidersForRequest(
registry: ProviderRegistry,
providerName?: string,
): Promise<IProvider[]> {
const available = await registry.getAvailable();
if (providerName) {
return available.filter((provider) => provider.name === providerName);
}
return available.filter((provider) => !EXPLICIT_ONLY_PROVIDERS.has(provider.name));
}

export async function loadProviderData(
providers: IProvider[],
range: DateRange,
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp/src/tools/compare-periods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import type { DateRange } from '@tokenleak/core';
import type { ProviderRegistry } from '@tokenleak/registry';
import { assertValidDate, validateRange } from '../shared/date-range.js';
import { loadProviderData } from '../shared/provider-load.js';
import { getAvailableProvidersForRequest, loadProviderData } from '../shared/provider-load.js';

async function loadAndAggregate(
providers: Awaited<ReturnType<ProviderRegistry['getAvailable']>>,
Expand Down Expand Up @@ -53,7 +53,7 @@ export async function handleComparePeriods(
previousRange = computePreviousPeriod(currentRange);
}

const available = await registry.getAvailable();
const available = await getAvailableProvidersForRequest(registry);

const [currentResult, previousResult] = await Promise.all([
loadAndAggregate(available, currentRange),
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp/src/tools/get-cost-breakdown.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { aggregate, mergeProviderData } from '@tokenleak/core';
import type { ProviderRegistry } from '@tokenleak/registry';
import { resolveRange } from '../shared/date-range.js';
import { loadProviderData } from '../shared/provider-load.js';
import { getAvailableProvidersForRequest, loadProviderData } from '../shared/provider-load.js';

export async function handleGetCostBreakdown(
args: { days?: number; since?: string; until?: string },
registry: ProviderRegistry,
) {
try {
const range = resolveRange(args);
const available = await registry.getAvailable();
const available = await getAvailableProvidersForRequest(registry);

const { data, warnings } = await loadProviderData(available, range);

Expand Down
7 changes: 2 additions & 5 deletions packages/mcp/src/tools/get-daily-usage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { buildDailyCostCompleteness, mergeProviderData } from '@tokenleak/core';
import type { ProviderRegistry } from '@tokenleak/registry';
import { resolveRange } from '../shared/date-range.js';
import { loadProviderData } from '../shared/provider-load.js';
import { getAvailableProvidersForRequest, loadProviderData } from '../shared/provider-load.js';

const DEFAULT_DAILY_DAYS = 14;

Expand All @@ -11,10 +11,7 @@ export async function handleGetDailyUsage(
) {
try {
const range = resolveRange(args, DEFAULT_DAILY_DAYS);
const available = await registry.getAvailable();
const filtered = args.provider
? available.filter((p) => p.name === args.provider)
: available;
const filtered = await getAvailableProvidersForRequest(registry, args.provider);

const { data, warnings } = await loadProviderData(filtered, range);

Expand Down
7 changes: 2 additions & 5 deletions packages/mcp/src/tools/get-efficiency-advice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,15 @@ import {
import { MODEL_PRICING } from '@tokenleak/registry';
import type { ProviderRegistry } from '@tokenleak/registry';
import { resolveRange } from '../shared/date-range.js';
import { loadProviderData } from '../shared/provider-load.js';
import { getAvailableProvidersForRequest, loadProviderData } from '../shared/provider-load.js';

export async function handleGetEfficiencyAdvice(
args: { days?: number; since?: string; until?: string; provider?: string },
registry: ProviderRegistry,
) {
try {
const range = resolveRange(args);
const available = await registry.getAvailable();
const filtered = args.provider
? available.filter((p) => p.name === args.provider)
: available;
const filtered = await getAvailableProvidersForRequest(registry, args.provider);

const { data, warnings } = await loadProviderData(filtered, range);

Expand Down
Loading
Loading