subrouter-cli 0.0.0-stage → 0.1.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/CLAUDE.md +51 -0
- package/README.md +397 -2
- package/config.example.json +9 -0
- package/package.json +21 -4
- package/scripts/sse-harness.ts +252 -0
- package/scripts/sub-wrapper.sh +11 -0
- package/src/adapters/anthropic.ts +311 -0
- package/src/adapters/openai.ts +227 -0
- package/src/approval.ts +21 -0
- package/src/chatviewport.ts +123 -0
- package/src/client.ts +398 -0
- package/src/clipboard.ts +62 -0
- package/src/commandpolicy.ts +272 -0
- package/src/commands.ts +18 -0
- package/src/config.ts +107 -0
- package/src/effort.ts +18 -0
- package/src/images.ts +77 -0
- package/src/index.ts +248 -0
- package/src/lineinput.ts +726 -0
- package/src/loop.ts +201 -0
- package/src/markdown.ts +244 -0
- package/src/repl.ts +700 -0
- package/src/sessions.ts +58 -0
- package/src/sse.ts +64 -0
- package/src/terminal.ts +228 -0
- package/src/token.ts +36 -0
- package/src/toolpreview.ts +54 -0
- package/src/tools.ts +790 -0
- package/src/types.ts +73 -0
- package/src/ui.ts +813 -0
- package/src/usage.ts +186 -0
- package/test/absolute-tools.test.ts +296 -0
- package/test/absolute-ui.test.ts +153 -0
- package/test/adapters.test.ts +205 -0
- package/test/anthropic.test.ts +246 -0
- package/test/auto-ui.test.ts +106 -0
- package/test/chatviewport.test.ts +49 -0
- package/test/client.test.ts +327 -0
- package/test/clipboard.test.ts +63 -0
- package/test/command-input.test.ts +183 -0
- package/test/command-tools.test.ts +141 -0
- package/test/commandpolicy.test.ts +252 -0
- package/test/disk-tools.test.ts +220 -0
- package/test/effort.test.ts +123 -0
- package/test/fixtures/openai-tools.sse +84 -0
- package/test/image-adapters.test.ts +78 -0
- package/test/image-ui.test.ts +151 -0
- package/test/images.test.ts +60 -0
- package/test/loop.test.ts +387 -0
- package/test/m5.test.ts +127 -0
- package/test/markdown.test.ts +201 -0
- package/test/repl-ui.test.ts +293 -0
- package/test/sse.test.ts +74 -0
- package/test/steering-ui.test.ts +182 -0
- package/test/steering.test.ts +68 -0
- package/test/terminal-ui.test.ts +144 -0
- package/test/terminal.test.ts +229 -0
- package/test/toolpreview.test.ts +51 -0
- package/test/tools.test.ts +227 -0
- package/test/ui.test.ts +635 -0
- package/test/usage-footer.test.ts +180 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { providerUsage, usageLimits, modelUsage, fetchUsage } from '../src/usage.ts';
|
|
4
|
+
|
|
5
|
+
describe('account limits and selected-provider report', () => {
|
|
6
|
+
it('labels shared quotas separately and preserves zero and unlimited', () => {
|
|
7
|
+
assert.equal(usageLimits({ user: {
|
|
8
|
+
rpm_limit: 60, today_tokens: 1000, daily_token_limit: 1000000,
|
|
9
|
+
week_usd: 1.5, weekly_usd_limit: 10,
|
|
10
|
+
} }), 'account: 60 rpm · day 1K/1M tokens · week $1.5/$10');
|
|
11
|
+
assert.equal(usageLimits({ quotas: {
|
|
12
|
+
rpm_limit: 0, today_tokens: 0, daily_token_limit: null, week_usd: 0, weekly_usd_limit: null,
|
|
13
|
+
} }), 'account: 0 rpm · day 0/unlimited tokens · week $0/unlimited');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('keeps large account totals compact and reports each provider window only once', () => {
|
|
17
|
+
const data = { user: { today_tokens: 390672620, daily_token_limit: null, week_usd: 224.068, weekly_usd_limit: 500 }, pools: [
|
|
18
|
+
{ provider: 'grok', window: '5h', used_percent: 10, resets_at: '2026-10-11T04:00:00Z' },
|
|
19
|
+
{ provider: 'grok', window: '7d', used_percent: 20 },
|
|
20
|
+
] };
|
|
21
|
+
assert.equal(usageLimits(data), 'account: day 390.7M/unlimited tokens · week $224.07/$500');
|
|
22
|
+
const report = modelUsage(data, 'grok/grok-4').join('\n');
|
|
23
|
+
assert.equal(report.split('5h').length - 1, 1);
|
|
24
|
+
assert.equal(report.split('7d').length - 1, 1);
|
|
25
|
+
assert.equal(report.split('390.7M').length - 1, 1);
|
|
26
|
+
assert.match(report, /resets 10-11 04:00/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('never invents missing account limits or coerces invalid amounts', () => {
|
|
30
|
+
for (const json of [undefined, null, [], {}, { user: [] }])
|
|
31
|
+
assert.equal(usageLimits(json), 'account limits: unknown');
|
|
32
|
+
for (const value of ['10', -1, NaN, Infinity, {}])
|
|
33
|
+
assert.equal(usageLimits({ user: { today_tokens: value, daily_token_limit: value } }), 'account: day ?/? tokens');
|
|
34
|
+
assert.equal(usageLimits({ user: { daily_token_limit: 10 } }), 'account: day ?/10 tokens');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('detailed view includes only the selected provider and handles invalid reset dates', () => {
|
|
38
|
+
const data = { user: { rpm_limit: 60 }, pools: [
|
|
39
|
+
{ provider: 'anthropic', window: '5h', used_percent: 20, resets_at: 1e100 },
|
|
40
|
+
{ provider: 'openai', window: '7d', used_percent: 10 },
|
|
41
|
+
] };
|
|
42
|
+
const report = modelUsage(data, 'anthropic/claude-test').join('\n');
|
|
43
|
+
assert.match(report, /anthropic: 5h 80% available/);
|
|
44
|
+
assert.match(report, /Account limits\n60 rpm/);
|
|
45
|
+
assert.match(report, /resets unknown/);
|
|
46
|
+
assert.ok(!report.includes('openai'));
|
|
47
|
+
assert.match(modelUsage(undefined, 'openai/test').join('\n'), /No provider pool limits reported/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('explicit refresh bypasses the ordinary 30-second usage cache', async () => {
|
|
51
|
+
let calls = 0;
|
|
52
|
+
const client = { usage: async () => ({ calls: ++calls }) };
|
|
53
|
+
const signal = new AbortController().signal;
|
|
54
|
+
const first = await fetchUsage(client as never, signal, true);
|
|
55
|
+
assert.deepEqual(await fetchUsage(client as never, signal), first);
|
|
56
|
+
assert.deepEqual(await fetchUsage(client as never, signal, true), { calls: 2 });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('providerUsage', () => {
|
|
61
|
+
it('filters by model prefix and keeps all matching pool windows', () => {
|
|
62
|
+
const json = {
|
|
63
|
+
user: { today_tokens: 123, rpm_limit: 60 },
|
|
64
|
+
pools: [
|
|
65
|
+
{ provider: 'anthropic', window: '5h', used_percent: 25, resets_at: '2026-10-11T04:00:00Z' },
|
|
66
|
+
{ provider: 'openai', window: 'weekly', used_percent: 99 },
|
|
67
|
+
{ provider: 'anthropic', window: '7d', used_percent: 57, resets_at: 1791763200 },
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
assert.equal(providerUsage(json, 'anthropic/claude-sonnet'),
|
|
71
|
+
'anthropic: 5h 75% · 7d 43% available');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('matches arbitrary explicit providers, with prefixes taking priority over model names', () => {
|
|
75
|
+
const json = { pools: [
|
|
76
|
+
{ provider: 'opencode', used_percent: 10 },
|
|
77
|
+
{ provider: 'deepseek', used_percent: 20 },
|
|
78
|
+
{ provider: 'openai', used_percent: 30 },
|
|
79
|
+
{ provider: 'anthropic', used_percent: 40 },
|
|
80
|
+
] };
|
|
81
|
+
assert.equal(providerUsage(json, 'opencode/deepseek-v4'), 'opencode: 90% available');
|
|
82
|
+
assert.equal(providerUsage(json, 'openai/claude-proxy'), 'openai: 70% available');
|
|
83
|
+
assert.equal(providerUsage(json, 'deepseek/deepseek-v4'), 'deepseek: 80% available');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('normalizes case/whitespace, supports pool name fallback, and aliases xai to grok both ways', () => {
|
|
87
|
+
assert.equal(providerUsage({ pools: [{ name: ' GROK ', used_percent: 40 }] }, ' XAI/grok-4 '),
|
|
88
|
+
'grok: 60% available');
|
|
89
|
+
assert.equal(providerUsage({ pools: [{ provider: 'xai', used_percent: 40 }] }, 'grok/grok-4'),
|
|
90
|
+
'grok: 60% available');
|
|
91
|
+
assert.equal(providerUsage({ pools: [{ provider: ' OpenAI ', used_percent: 20 }] }, ' OPENAI/gpt-5 '),
|
|
92
|
+
'openai: 80% available');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('recognizes supported unprefixed model families, but never guesses unknown families', () => {
|
|
96
|
+
for (const [model, provider] of [
|
|
97
|
+
['claude-sonnet', 'anthropic'], ['claude.sonnet', 'anthropic'],
|
|
98
|
+
['gpt-5', 'openai'], ['grok-4', 'grok'], ['deepseek-v4', 'deepseek'],
|
|
99
|
+
]) {
|
|
100
|
+
assert.equal(providerUsage({ pools: [{ provider, used_percent: 0 }] }, model),
|
|
101
|
+
`${provider}: 100% available`);
|
|
102
|
+
}
|
|
103
|
+
assert.equal(providerUsage({ pools: [{ provider: 'openai', used_percent: 0 }] }, 'mystery'), 'usage: unknown');
|
|
104
|
+
assert.equal(providerUsage({ pools: [{ provider: '', used_percent: 0 }] }, ''), 'usage: unknown');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('distinguishes missing percentages from genuine empty and fully available pools', () => {
|
|
108
|
+
assert.equal(providerUsage({ pools: [
|
|
109
|
+
{ provider: 'anthropic', window: 'zero', used_percent: 0 },
|
|
110
|
+
{ provider: 'anthropic', window: 'full', used_percent: 100 },
|
|
111
|
+
{ provider: 'anthropic', window: 'missing' },
|
|
112
|
+
] }, 'anthropic/claude'),
|
|
113
|
+
'anthropic: zero 100% · full 0% · missing unknown available');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('does not coerce, clamp, or invent percentages for invalid stats', () => {
|
|
117
|
+
for (const used_percent of [null, '25', NaN, Infinity, -1, 101, {}, []]) {
|
|
118
|
+
assert.equal(providerUsage({ pools: [{ provider: 'openai', used_percent }] }, 'openai/gpt'),
|
|
119
|
+
'openai: unknown available');
|
|
120
|
+
}
|
|
121
|
+
assert.equal(providerUsage({ pools: [{ provider: 'openai', used_percent: 33.3 }] }, 'openai/gpt'),
|
|
122
|
+
'openai: 67% available');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('omits reset metadata whether valid, missing, or malformed', () => {
|
|
126
|
+
for (const resets_at of [undefined, null, 'nope', NaN, Infinity, 1e100, {}, [],
|
|
127
|
+
0, 1791763200, '2026-10-11T06:00:00+02:00', '2026-10-11']) {
|
|
128
|
+
assert.equal(providerUsage({ pools: [{ provider: 'grok', used_percent: 0, resets_at }] }, 'grok/grok-4'),
|
|
129
|
+
'grok: 100% available');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('falls back to live user quotas only when no provider pool matches', () => {
|
|
134
|
+
const json = {
|
|
135
|
+
user: { today_tokens: 0, daily_token_limit: null, rpm_limit: 0 },
|
|
136
|
+
pools: [{ provider: 'anthropic', used_percent: 0 }],
|
|
137
|
+
};
|
|
138
|
+
assert.equal(providerUsage(json, 'openai/gpt-5'), 'quota: 0 / unlimited tokens today · 0 rpm');
|
|
139
|
+
assert.equal(providerUsage(json, 'anthropic/claude'), 'anthropic: 100% available');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('supports documented quotas, but does not infer today tokens from undocumented aliases', () => {
|
|
143
|
+
assert.equal(providerUsage({ quotas: { daily_token_limit: 1000000, daily_tokens_used: 25, rpm_limit: 60 } }, 'grok/grok-4'),
|
|
144
|
+
'quota: unknown / 1,000,000 tokens today · 60 rpm');
|
|
145
|
+
assert.equal(providerUsage({ user: { today_tokens: 250000 } }, 'deepseek/deepseek-v4'),
|
|
146
|
+
'quota: 250,000 / unknown tokens today');
|
|
147
|
+
assert.equal(providerUsage({ user: { daily_token_limit: 0 } }, 'openai/gpt-5'),
|
|
148
|
+
'quota: unknown / 0 tokens today');
|
|
149
|
+
assert.equal(providerUsage({ user: { rpm_limit: '60' } }, 'openai/gpt-5'), 'quota: unknown rpm');
|
|
150
|
+
assert.equal(providerUsage({ user: { today_tokens: null, daily_token_limit: Infinity, rpm_limit: null } }, 'openai/gpt-5'),
|
|
151
|
+
'quota: unknown / unknown tokens today · unknown rpm');
|
|
152
|
+
assert.equal(providerUsage({ user: { rpm_limit: 10 }, quotas: { rpm_limit: 60 } }, 'openai/gpt-5'), 'quota: 10 rpm');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('tolerates malformed payloads and pool entries', () => {
|
|
156
|
+
for (const json of [undefined, null, false, 0, 'nope', [], {}, { pools: 'nope' }, { user: [] }]) {
|
|
157
|
+
assert.equal(providerUsage(json, 'openai/gpt-5'), 'usage: unknown');
|
|
158
|
+
}
|
|
159
|
+
assert.equal(providerUsage({ pools: [null, 0, 'nope', [], { provider: 123 }, { provider: 'openai-extra', used_percent: 10 }] }, 'openai/gpt-5'),
|
|
160
|
+
'usage: unknown');
|
|
161
|
+
assert.equal(providerUsage({ pools: [null, { provider: 'openai', window: ' 5h\n weekly ', used_percent: 10 }] }, 'openai/gpt-5'),
|
|
162
|
+
'openai: 5h weekly 90% available');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('is deterministic, does not mutate input, and never fetches', () => {
|
|
166
|
+
const pool = Object.freeze({ provider: 'openai', window: '5h', used_percent: 0, resets_at: 0 });
|
|
167
|
+
const json = Object.freeze({ pools: Object.freeze([pool]) });
|
|
168
|
+
const before = JSON.stringify(json);
|
|
169
|
+
const originalFetch = globalThis.fetch;
|
|
170
|
+
globalThis.fetch = async () => { throw new Error('unexpected fetch'); };
|
|
171
|
+
try {
|
|
172
|
+
const first = providerUsage(json, 'openai/gpt-5');
|
|
173
|
+
assert.equal(providerUsage(json, 'openai/gpt-5'), first);
|
|
174
|
+
assert.equal(first, 'openai: 5h 100% available');
|
|
175
|
+
assert.equal(JSON.stringify(json), before);
|
|
176
|
+
} finally {
|
|
177
|
+
globalThis.fetch = originalFetch;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "esnext",
|
|
4
|
+
"lib": ["esnext"],
|
|
5
|
+
"module": "nodenext",
|
|
6
|
+
"moduleResolution": "nodenext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"erasableSyntaxOnly": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src", "scripts", "test"]
|
|
17
|
+
}
|