Agentic coding CLI on Bun, Ink, and the AI SDK. Core: streamText loop with SDK-level tool approval so a denied call provably never executes; endpoint fallback for OpenAI reasoning models; retry with backoff. Tools: read/write/edit/glob/grep/bash, path-jailed, gitignore-aware, ripgrep with a JS fallback, binary rejection, live bash streaming. Agents: five variants crossing thinking level with tool restriction; plan and review withhold mutating tools from the model. Extensibility: frontmatter skills with on-demand bodies, plugin host with blocking hooks, MCP stdio and HTTP, read-only subagents. State: durable per-project memory, session task lists, session persistence, compaction that repairs provider-item dependencies. Distribution: five-platform cross-compiled binaries with checksums, install scripts, CI on three operating systems. 404 tests, typecheck clean.
60 lines
2.2 KiB
TypeScript
60 lines
2.2 KiB
TypeScript
import { expect, test } from 'bun:test';
|
|
import { costOf, formatUsd, rateFor, usageLine } from '../src/pricing';
|
|
|
|
test('a known model resolves to a rate', () => {
|
|
expect(rateFor('gpt-5')).toEqual({ inputPerMTok: 1.25, outputPerMTok: 10 });
|
|
expect(rateFor('claude-sonnet-4-5')).toEqual({ inputPerMTok: 3, outputPerMTok: 15 });
|
|
});
|
|
|
|
test('a dated model id resolves through its prefix', () => {
|
|
expect(rateFor('claude-sonnet-4-5-20250929')).toEqual(rateFor('claude-sonnet-4'));
|
|
});
|
|
|
|
test('the longest matching prefix wins so mini does not resolve to the base model', () => {
|
|
expect(rateFor('gpt-5-mini')?.inputPerMTok).toBe(0.25);
|
|
expect(rateFor('gpt-5')?.inputPerMTok).toBe(1.25);
|
|
expect(rateFor('gpt-5-nano')?.inputPerMTok).toBe(0.05);
|
|
});
|
|
|
|
test('an OpenRouter-style prefixed id is stripped before matching', () => {
|
|
expect(rateFor('anthropic/claude-sonnet-4-5')).toEqual(rateFor('claude-sonnet-4-5'));
|
|
expect(rateFor('openai/gpt-5-mini')).toEqual(rateFor('gpt-5-mini'));
|
|
});
|
|
|
|
test('matching is case-insensitive', () => {
|
|
expect(rateFor('GPT-5')).toEqual(rateFor('gpt-5'));
|
|
});
|
|
|
|
test('an unknown model has no rate and no cost', () => {
|
|
expect(rateFor('some-local-llama')).toBeUndefined();
|
|
expect(costOf('some-local-llama', 1000, 500)).toBeUndefined();
|
|
});
|
|
|
|
test('cost is computed per million tokens', () => {
|
|
// 1M in at $1.25 plus 1M out at $10.
|
|
expect(costOf('gpt-5', 1_000_000, 1_000_000)).toBeCloseTo(11.25, 6);
|
|
expect(costOf('gpt-5', 500_000, 0)).toBeCloseTo(0.625, 6);
|
|
expect(costOf('gpt-5', 0, 0)).toBe(0);
|
|
});
|
|
|
|
test('formatUsd keeps sub-cent amounts visible', () => {
|
|
expect(formatUsd(0)).toBe('$0.00');
|
|
expect(formatUsd(0.0001234)).toBe('$0.0001');
|
|
expect(formatUsd(0.005)).toBe('$0.0050');
|
|
expect(formatUsd(1.239)).toBe('$1.24');
|
|
expect(formatUsd(12)).toBe('$12.00');
|
|
});
|
|
|
|
test('usageLine reports tokens plus dollars for a priced model', () => {
|
|
const line = usageLine('gpt-5', 10_000, 2_000);
|
|
expect(line).toContain('10000 in / 2000 out tokens');
|
|
expect(line).toContain('$0.03');
|
|
});
|
|
|
|
test('usageLine says so plainly when the model is unpriced', () => {
|
|
const line = usageLine('qwen-local', 10, 5);
|
|
expect(line).toContain('10 in / 5 out tokens');
|
|
expect(line).toContain('unpriced');
|
|
expect(line).not.toContain('$');
|
|
});
|