@@ -0,0 +1,196 @@
import { describe , it , beforeEach } from 'node:test' ;
import assert from 'node:assert/strict' ;
import {
extractUsage ,
recordUsage ,
getRunUsage ,
resetRunUsage ,
recordRateLimit ,
getRateLimit ,
resetRateLimit ,
resolveRemainingPercent ,
fetchAccountQuota ,
formatUsageStats ,
formatUsageStatsLine ,
} from './usage.js' ;
describe ( 'extractUsage' , ( ) => {
it ( 'parses OpenAI-compatible usage' , ( ) => {
const u = extractUsage ( { usage : { prompt _tokens : 100 , completion _tokens : 20 , total _tokens : 120 } } ) ;
assert . deepEqual ( u , { promptTokens : 100 , completionTokens : 20 , totalTokens : 120 } ) ;
} ) ;
it ( 'parses OpenAI Responses input/output tokens and derives total' , ( ) => {
const u = extractUsage ( { usage : { input _tokens : 50 , output _tokens : 10 } } ) ;
assert . deepEqual ( u , { promptTokens : 50 , completionTokens : 10 , totalTokens : 60 } ) ;
} ) ;
it ( 'parses Gemini usageMetadata' , ( ) => {
const u = extractUsage ( { usageMetadata : { promptTokenCount : 30 , candidatesTokenCount : 5 , totalTokenCount : 35 } } ) ;
assert . deepEqual ( u , { promptTokens : 30 , completionTokens : 5 , totalTokens : 35 } ) ;
} ) ;
it ( 'parses Ollama native eval counts' , ( ) => {
const u = extractUsage ( { prompt _eval _count : 12 , eval _count : 8 } ) ;
assert . deepEqual ( u , { promptTokens : 12 , completionTokens : 8 , totalTokens : 20 } ) ;
} ) ;
it ( 'parses OpenCode tokens from info.tokens' , ( ) => {
const u = extractUsage ( { info : { tokens : { input : 7 , output : 3 } } } ) ;
assert . deepEqual ( u , { promptTokens : 7 , completionTokens : 3 , totalTokens : 10 } ) ;
} ) ;
it ( 'returns null when no usage info is present' , ( ) => {
assert . equal ( extractUsage ( { choices : [ { message : { content : 'hi' } } ] } ) , null ) ;
assert . equal ( extractUsage ( null ) , null ) ;
} ) ;
} ) ;
describe ( 'recordUsage / getRunUsage' , ( ) => {
beforeEach ( ( ) => resetRunUsage ( ) ) ;
it ( 'accumulates across calls and counts every call' , ( ) => {
recordUsage ( { usage : { prompt _tokens : 10 , completion _tokens : 2 , total _tokens : 12 } } ) ;
recordUsage ( { usage : { prompt _tokens : 5 , completion _tokens : 1 , total _tokens : 6 } } ) ;
recordUsage ( { parts : [ ] } ) ; // no usage → still counts as a call
assert . deepEqual ( getRunUsage ( ) , { calls : 3 , promptTokens : 15 , completionTokens : 3 , totalTokens : 18 } ) ;
} ) ;
it ( 'returns a copy, not the internal object' , ( ) => {
const a = getRunUsage ( ) ;
a . calls = 999 ;
assert . equal ( getRunUsage ( ) . calls , 0 ) ;
} ) ;
} ) ;
describe ( 'fetchAccountQuota' , ( ) => {
it ( 'reads OpenRouter credits via injected get' , async ( ) => {
const get = async ( url , opts ) => {
assert . match ( url , /openrouter\.ai\/api\/v1\/auth\/key$/ ) ;
assert . equal ( opts . headers . Authorization , 'Bearer sk-or-xxx' ) ;
return { data : { data : { usage : 12.4 , limit : 100 , limit _remaining : 87.6 } } } ;
} ;
const q = await fetchAccountQuota ( 'openai' , { apiKeys : [ 'sk-or-xxx' ] , baseURL : 'https://openrouter.ai/api/v1' } , { get } ) ;
assert . deepEqual ( q , { available : true , used : 12.4 , limit : 100 , remaining : 87.6 , currency : 'USD' , source : 'openrouter' } ) ;
} ) ;
it ( 'derives remaining when OpenRouter omits limit_remaining' , async ( ) => {
const get = async ( ) => ( { data : { data : { usage : 10 , limit : 50 } } } ) ;
const q = await fetchAccountQuota ( 'openai' , { apiKeys : [ 'k' ] , baseURL : 'https://openrouter.ai/api/v1' } , { get } ) ;
assert . equal ( q . remaining , 40 ) ;
} ) ;
it ( 'reports unavailable for plain OpenAI (no openrouter)' , async ( ) => {
const q = await fetchAccountQuota ( 'openai' , { apiKeys : [ 'k' ] , baseURL : 'https://api.openai.com/v1' } , { get : async ( ) => { throw new Error ( 'should not call' ) ; } } ) ;
assert . equal ( q . available , false ) ;
assert . match ( q . reason , /API key 無法取得/ ) ;
} ) ;
it ( 'reports 不適用 for local platforms' , async ( ) => {
assert . equal ( ( await fetchAccountQuota ( 'ollama' , { } ) ) . available , false ) ;
assert . equal ( ( await fetchAccountQuota ( 'opencode' , { } ) ) . available , false ) ;
} ) ;
it ( 'degrades gracefully when the quota call throws' , async ( ) => {
const get = async ( ) => { throw new Error ( 'network down' ) ; } ;
const q = await fetchAccountQuota ( 'openai' , { apiKeys : [ 'k' ] , baseURL : 'https://openrouter.ai/api/v1' } , { get } ) ;
assert . deepEqual ( q , { available : false , reason : 'network down' } ) ;
} ) ;
it ( 'reports unsupported provider' , async ( ) => {
const q = await fetchAccountQuota ( 'mystery' , { } ) ;
assert . equal ( q . available , false ) ;
assert . match ( q . reason , /未支援/ ) ;
} ) ;
} ) ;
describe ( 'recordRateLimit / getRateLimit' , ( ) => {
beforeEach ( ( ) => resetRateLimit ( ) ) ;
it ( 'captures OpenAI-style token rate-limit headers (case-insensitive)' , ( ) => {
recordRateLimit ( { 'X-RateLimit-Remaining-Tokens' : '190000' , 'X-RateLimit-Limit-Tokens' : '200000' } ) ;
assert . deepEqual ( getRateLimit ( ) , { hasData : true , remaining : 190000 , limit : 200000 , kind : 'tokens' } ) ;
} ) ;
it ( 'captures Anthropic-style token rate-limit headers' , ( ) => {
recordRateLimit ( { 'anthropic-ratelimit-tokens-remaining' : '8000' , 'anthropic-ratelimit-tokens-limit' : '10000' } ) ;
assert . deepEqual ( getRateLimit ( ) , { hasData : true , remaining : 8000 , limit : 10000 , kind : 'tokens' } ) ;
} ) ;
it ( 'falls back to request-dimension headers when token headers are absent' , ( ) => {
recordRateLimit ( { 'x-ratelimit-remaining-requests' : '45' , 'x-ratelimit-limit-requests' : '60' } ) ;
assert . deepEqual ( getRateLimit ( ) , { hasData : true , remaining : 45 , limit : 60 , kind : 'requests' } ) ;
} ) ;
it ( 'ignores responses without rate-limit headers' , ( ) => {
recordRateLimit ( { 'content-type' : 'application/json' } ) ;
assert . equal ( getRateLimit ( ) . hasData , false ) ;
} ) ;
} ) ;
describe ( 'resolveRemainingPercent' , ( ) => {
it ( 'prefers account quota when a finite limit exists' , ( ) => {
const pct = resolveRemainingPercent ( { available : true , used : 12.4 , limit : 100 , remaining : 87.6 , currency : 'USD' } , { hasData : true , remaining : 1 , limit : 10 , kind : 'tokens' } ) ;
assert . equal ( pct . percent , 87.6 ) ;
assert . equal ( pct . basis , '帳號額度' ) ;
} ) ;
it ( 'derives remaining from used when quota.remaining is absent' , ( ) => {
const pct = resolveRemainingPercent ( { available : true , used : 25 , limit : 100 , currency : 'USD' } , null ) ;
assert . equal ( pct . percent , 75 ) ;
} ) ;
it ( 'falls back to rate-limit window percent when quota has no limit' , ( ) => {
const pct = resolveRemainingPercent ( { available : false , reason : 'x' } , { hasData : true , remaining : 150000 , limit : 200000 , kind : 'tokens' } ) ;
assert . equal ( pct . percent , 75 ) ;
assert . match ( pct . basis , /速率配額(當前視窗,token) / ) ;
} ) ;
it ( 'returns null percent with a reason when nothing is available' , ( ) => {
const pct = resolveRemainingPercent ( { available : false , reason : '本地服務,無帳號額度概念' } , { hasData : false } ) ;
assert . equal ( pct . percent , null ) ;
assert . equal ( pct . reason , '本地服務,無帳號額度概念' ) ;
} ) ;
it ( 'explains an unlimited account cannot yield a percent' , ( ) => {
const pct = resolveRemainingPercent ( { available : true , used : 5 , limit : null } , { hasData : false } ) ;
assert . equal ( pct . percent , null ) ;
assert . match ( pct . reason , /無上限/ ) ;
} ) ;
} ) ;
describe ( 'formatUsageStats' , ( ) => {
const usage = { calls : 7 , promptTokens : 18432 , completionTokens : 2107 , totalTokens : 20539 } ;
it ( 'renders token table and remaining percent from account quota' , ( ) => {
const out = formatUsageStats ( 'openai' , 'gpt-4o-mini' , usage , { available : true , used : 12.4 , limit : 100 , remaining : 87.6 , currency : 'USD' } , null ) ;
assert . match ( out , /## 🤖 AI 助理使用量/ ) ;
assert . match ( out , /18,432 \| 2,107 \| 20,539/ ) ;
assert . match ( out , /共 7 次呼叫/ ) ;
assert . match ( out , /剩餘可用 \*\*87.6%\*\*(帳號額度:USD 87.6 \/ USD 100) / ) ;
} ) ;
it ( 'renders remaining percent from rate-limit window when quota is unavailable' , ( ) => {
const out = formatUsageStats ( 'claude' , 'sonnet' , usage , { available : false , reason : 'r' } , { hasData : true , remaining : 150000 , limit : 200000 , kind : 'tokens' } ) ;
assert . match ( out , /剩餘可用 \*\*75%\*\*(速率配額(當前視窗,token):150,000 \/ 200,000) / ) ;
} ) ;
it ( 'explains when no percentage can be computed' , ( ) => {
const out = formatUsageStats ( 'ollama' , 'llama3' , usage , { available : false , reason : '本地服務,無帳號額度概念' } , { hasData : false } ) ;
assert . match ( out , /剩餘可用:無法計算百分比(本地服務,無帳號額度概念)/ ) ;
} ) ;
} ) ;
describe ( 'formatUsageStatsLine' , ( ) => {
const usage = { calls : 3 , promptTokens : 100 , completionTokens : 20 , totalTokens : 120 } ;
it ( 'summarises tokens and remaining percent on one line' , ( ) => {
const line = formatUsageStatsLine ( 'openai' , 'gpt-4o-mini' , usage , { available : true , used : 1 , limit : 10 , remaining : 9 , currency : 'USD' } , null ) ;
assert . equal ( line , '本次 openai/gpt-4o-mini: 提示100 + 回應20 = 120 token( 3 次呼叫);剩餘可用: 90%(帳號額度 USD 9/USD 10) ' ) ;
} ) ;
it ( 'notes when remaining percent cannot be computed' , ( ) => {
const line = formatUsageStatsLine ( 'ollama' , 'llama3' , usage , { available : false , reason : '本地服務,無帳號額度概念' } , { hasData : false } ) ;
assert . match ( line , /;剩餘可用: 無法計算(本地服務,無帳號額度概念)/ ) ;
} ) ;
} ) ;