Files
Muhammad Zakir Ramadhan 5b8503fcd9 Initial commit: shiro-neko 0.1.0-beta.1
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.
2026-09-02 17:30:18 +07:00

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('$');
});