ai-xray 1.2.0 → 2.0.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/PRD.md +421 -280
- package/README.md +2 -2
- package/dist/cli.js +771 -0
- package/dist/cli.js.map +1 -0
- package/package.json +36 -24
- package/src/cli.ts +155 -118
- package/src/client.ts +203 -0
- package/src/commands/bench.ts +99 -0
- package/src/commands/compare.ts +76 -0
- package/src/commands/id.ts +139 -0
- package/src/commands/ping.ts +55 -0
- package/src/commands/probe.ts +136 -0
- package/src/commands/tokenize.ts +96 -0
- package/src/utils/http.ts +86 -0
- package/src/utils/output.ts +36 -123
- package/src/utils/timer.ts +75 -0
- package/tests/bench.test.ts +13 -0
- package/tests/client.test.ts +37 -0
- package/tests/compare.test.ts +24 -0
- package/tests/http.test.ts +12 -0
- package/tests/id.test.ts +13 -0
- package/tests/ping.test.ts +12 -0
- package/tests/probe.test.ts +13 -0
- package/tests/tokenize.test.ts +32 -0
- package/tsup.config.ts +11 -11
- package/vitest.config.ts +13 -0
- package/ana-suggestions.md +0 -105
- package/tests/cli.test.ts +0 -172
- package/tests/diff.test.ts +0 -169
- package/tests/env.test.ts +0 -69
- package/tests/init.test.ts +0 -164
- package/tests/output.test.ts +0 -49
- package/tests/read.test.ts +0 -169
- package/tests/scout.test.ts +0 -248
- package/tests/tree.test.ts +0 -222
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
|
|
3
|
+
export interface TokenResult {
|
|
4
|
+
characters: number;
|
|
5
|
+
words: number;
|
|
6
|
+
estimated_tokens: number;
|
|
7
|
+
cost_estimate?: {
|
|
8
|
+
model: string;
|
|
9
|
+
input_cost_usd: number;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Simple BPE-style token estimation
|
|
14
|
+
// Average: 4 characters per token for English, but varies by content
|
|
15
|
+
function estimateTokens(text: string): number {
|
|
16
|
+
// Count tokens more accurately by considering:
|
|
17
|
+
// - Word boundaries (~1 token per word on average)
|
|
18
|
+
// - Punctuation (~1 token per ~4 chars)
|
|
19
|
+
// - Numbers (~1 token per number sequence)
|
|
20
|
+
|
|
21
|
+
const words = text.split(/\s+/).filter(w => w.length > 0);
|
|
22
|
+
const wordCount = words.length;
|
|
23
|
+
|
|
24
|
+
// Base estimation: words + chars/4
|
|
25
|
+
const charBased = Math.ceil(text.length / 4);
|
|
26
|
+
const wordBased = Math.ceil(wordCount * 1.3); // ~1.3 tokens per word
|
|
27
|
+
|
|
28
|
+
// Use average of both approaches
|
|
29
|
+
const estimated = Math.round((charBased + wordBased) / 2);
|
|
30
|
+
|
|
31
|
+
return Math.max(1, estimated);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pricing constants (USD per 1M tokens) - approximate
|
|
35
|
+
const MODEL_PRICING: Record<string, number> = {
|
|
36
|
+
'gpt-4o': 2.50, // $2.50 per 1M input tokens
|
|
37
|
+
'gpt-4o-mini': 0.15,
|
|
38
|
+
'gpt-4-turbo': 10.00,
|
|
39
|
+
'gpt-4': 30.00,
|
|
40
|
+
'gpt-3.5-turbo': 0.50,
|
|
41
|
+
'claude-3-5-sonnet-2024': 3.00,
|
|
42
|
+
'claude-3-opus-2024': 15.00,
|
|
43
|
+
'claude-3-haiku-2024': 0.25,
|
|
44
|
+
'gemini-1.5-pro': 1.25,
|
|
45
|
+
'gemini-1.5-flash': 0.075,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function getModelPrice(modelName: string): number | null {
|
|
49
|
+
const lowerModel = modelName.toLowerCase();
|
|
50
|
+
for (const [key, price] of Object.entries(MODEL_PRICING)) {
|
|
51
|
+
if (lowerModel.includes(key)) {
|
|
52
|
+
return price;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function countTokens(
|
|
59
|
+
input: string,
|
|
60
|
+
options?: { model?: string }
|
|
61
|
+
): Promise<TokenResult> {
|
|
62
|
+
let text = input;
|
|
63
|
+
|
|
64
|
+
// If input looks like a file path, read the file
|
|
65
|
+
if (!input.includes('\n') && fs.existsSync(input)) {
|
|
66
|
+
try {
|
|
67
|
+
text = fs.readFileSync(input, 'utf-8');
|
|
68
|
+
} catch {
|
|
69
|
+
// Keep original input if file read fails
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const characters = text.length;
|
|
74
|
+
const words = text.split(/\s+/).filter(w => w.length > 0).length;
|
|
75
|
+
const estimatedTokens = estimateTokens(text);
|
|
76
|
+
|
|
77
|
+
const result: TokenResult = {
|
|
78
|
+
characters,
|
|
79
|
+
words,
|
|
80
|
+
estimated_tokens: estimatedTokens,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Add cost estimate if model specified
|
|
84
|
+
if (options?.model) {
|
|
85
|
+
const price = getModelPrice(options.model);
|
|
86
|
+
if (price !== null) {
|
|
87
|
+
const cost = (estimatedTokens / 1000000) * price;
|
|
88
|
+
result.cost_estimate = {
|
|
89
|
+
model: options.model,
|
|
90
|
+
input_cost_usd: parseFloat(cost.toFixed(5)),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as https from 'https';
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
|
|
5
|
+
export interface HttpRequestOptions {
|
|
6
|
+
method?: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
body?: string;
|
|
9
|
+
timeout?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HttpResponse {
|
|
13
|
+
statusCode: number;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
body: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function request(
|
|
19
|
+
urlString: string,
|
|
20
|
+
options: HttpRequestOptions = {}
|
|
21
|
+
): Promise<HttpResponse> {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const url = new URL(urlString);
|
|
24
|
+
const isHttps = url.protocol === 'https:';
|
|
25
|
+
const client = isHttps ? https : http;
|
|
26
|
+
|
|
27
|
+
const requestOptions: http.RequestOptions = {
|
|
28
|
+
hostname: url.hostname,
|
|
29
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
30
|
+
path: url.pathname + url.search,
|
|
31
|
+
method: options.method || 'GET',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
'Accept': 'application/json',
|
|
35
|
+
...options.headers,
|
|
36
|
+
},
|
|
37
|
+
timeout: options.timeout || 30000,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const req = client.request(requestOptions, (res) => {
|
|
41
|
+
let body = '';
|
|
42
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
43
|
+
res.on('end', () => {
|
|
44
|
+
const headers: Record<string, string> = {};
|
|
45
|
+
for (const [key, value] of Object.entries(res.headers)) {
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
headers[key] = value;
|
|
48
|
+
} else if (Array.isArray(value)) {
|
|
49
|
+
headers[key] = value.join(', ');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
resolve({
|
|
53
|
+
statusCode: res.statusCode || 0,
|
|
54
|
+
headers,
|
|
55
|
+
body,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
req.on('error', reject);
|
|
61
|
+
req.on('timeout', () => {
|
|
62
|
+
req.destroy();
|
|
63
|
+
reject(new Error('Request timeout'));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (options.body) {
|
|
67
|
+
req.write(options.body);
|
|
68
|
+
}
|
|
69
|
+
req.end();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function requestJson<T = unknown>(
|
|
74
|
+
urlString: string,
|
|
75
|
+
options: HttpRequestOptions = {}
|
|
76
|
+
): Promise<{ response: HttpResponse; data: T }> {
|
|
77
|
+
return request(urlString, options).then((response) => {
|
|
78
|
+
let data: T;
|
|
79
|
+
try {
|
|
80
|
+
data = JSON.parse(response.body) as T;
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error(`Invalid JSON response: ${response.body.slice(0, 100)}`);
|
|
83
|
+
}
|
|
84
|
+
return { response, data };
|
|
85
|
+
});
|
|
86
|
+
}
|
package/src/utils/output.ts
CHANGED
|
@@ -1,123 +1,36 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core output management for ai-xray.
|
|
3
|
-
* Guarantees Machine-First protocol: Pure JSON on stdout, structured error on stderr.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (Array.isArray(pruned[key]) && pruned[key].length > 0) {
|
|
38
|
-
// Trim array items from the end until under budget
|
|
39
|
-
const arr = [...pruned[key]];
|
|
40
|
-
while (arr.length > 0 && JSON.stringify({ ...pruned, [key]: arr }).length > budgetChars) {
|
|
41
|
-
arr.pop();
|
|
42
|
-
}
|
|
43
|
-
pruned[key] = arr.length > 0 ? arr : `[TRUNCATED: ${pruned[key].length} items removed]`;
|
|
44
|
-
} else if (typeof pruned[key] === 'string' && pruned[key].length > 200) {
|
|
45
|
-
// Truncate long string values
|
|
46
|
-
const allowedLen = Math.max(100, pruned[key].length - (currentSize - budgetChars));
|
|
47
|
-
pruned[key] = pruned[key].slice(0, allowedLen) + '...[TRUNCATED]';
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Final fallback: if still over budget, hard-truncate the JSON string
|
|
52
|
-
const prunedJson = JSON.stringify(pruned);
|
|
53
|
-
if (prunedJson.length > budgetChars) {
|
|
54
|
-
const truncatedStr = prunedJson.slice(0, budgetChars);
|
|
55
|
-
// Try to parse back; if not valid, wrap as raw truncated output
|
|
56
|
-
return {
|
|
57
|
-
_truncated_raw: truncatedStr,
|
|
58
|
-
_meta: {
|
|
59
|
-
tokensEstimate: estimated,
|
|
60
|
-
budget,
|
|
61
|
-
truncated: true,
|
|
62
|
-
hint: `Output exceeded budget of ${budget} tokens. Data was hard-truncated.`
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
...pruned,
|
|
69
|
-
_meta: {
|
|
70
|
-
...(pruned._meta || {}),
|
|
71
|
-
tokensEstimate: estimateTokens(JSON.stringify(pruned)),
|
|
72
|
-
budget,
|
|
73
|
-
truncated: true,
|
|
74
|
-
hint: `Output exceeded budget of ${budget} tokens. Some fields were pruned.`
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Non-object data: hard truncate the serialized string
|
|
80
|
-
const truncated = serialized.slice(0, budgetChars);
|
|
81
|
-
return {
|
|
82
|
-
_truncated_raw: truncated,
|
|
83
|
-
_meta: {
|
|
84
|
-
tokensEstimate: estimated,
|
|
85
|
-
budget,
|
|
86
|
-
truncated: true,
|
|
87
|
-
hint: `Output exceeded budget of ${budget} tokens. Data was hard-truncated.`
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Under budget — attach meta info
|
|
93
|
-
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
94
|
-
return {
|
|
95
|
-
...data,
|
|
96
|
-
_meta: {
|
|
97
|
-
...(data._meta || {}),
|
|
98
|
-
tokensEstimate: estimated,
|
|
99
|
-
budget,
|
|
100
|
-
truncated: false
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return data;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function outputSuccess(data: any, budget?: number) {
|
|
109
|
-
const finalData = applyBudget(data, budget);
|
|
110
|
-
const output = JSON.stringify(finalData, null, process.argv.includes('--pretty') ? 2 : undefined) + '\n';
|
|
111
|
-
process.stdout.write(output, () => process.exit(0));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function outputError(error: unknown, code = 1) {
|
|
115
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
-
const errorObj = {
|
|
117
|
-
error: message,
|
|
118
|
-
code: code
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
process.stderr.write(JSON.stringify(errorObj, null, process.argv.includes('--pretty') ? 2 : undefined) + '\n');
|
|
122
|
-
process.exit(code);
|
|
123
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Core output management for ai-xray v2.0.0
|
|
3
|
+
* Guarantees Machine-First protocol: Pure JSON on stdout, structured error on stderr.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let prettyMode = false;
|
|
7
|
+
|
|
8
|
+
export function setPrettyMode(enabled: boolean): void {
|
|
9
|
+
prettyMode = enabled;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isPrettyMode(): boolean {
|
|
13
|
+
return prettyMode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function outputSuccess(data: unknown): void {
|
|
17
|
+
const output = JSON.stringify(data, null, prettyMode ? 2 : undefined) + '\n';
|
|
18
|
+
process.stdout.write(output);
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function outputError(error: unknown, code = 1): void {
|
|
23
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
+
const errorObj = {
|
|
25
|
+
error: message,
|
|
26
|
+
code
|
|
27
|
+
};
|
|
28
|
+
const output = JSON.stringify(errorObj, null, prettyMode ? 2 : undefined) + '\n';
|
|
29
|
+
process.stderr.write(output);
|
|
30
|
+
process.exit(code);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function outputJson(data: unknown): void {
|
|
34
|
+
const output = JSON.stringify(data, null, prettyMode ? 2 : undefined) + '\n';
|
|
35
|
+
process.stdout.write(output);
|
|
36
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export class Timer {
|
|
2
|
+
private startTime: number = 0;
|
|
3
|
+
private endTime: number = 0;
|
|
4
|
+
private running: boolean = false;
|
|
5
|
+
|
|
6
|
+
start(): void {
|
|
7
|
+
this.startTime = performance.now();
|
|
8
|
+
this.running = true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
stop(): number {
|
|
12
|
+
if (!this.running) {
|
|
13
|
+
throw new Error('Timer is not running');
|
|
14
|
+
}
|
|
15
|
+
this.endTime = performance.now();
|
|
16
|
+
this.running = false;
|
|
17
|
+
return this.elapsed();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
elapsed(): number {
|
|
21
|
+
if (this.running) {
|
|
22
|
+
return performance.now() - this.startTime;
|
|
23
|
+
}
|
|
24
|
+
return this.endTime - this.startTime;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
reset(): void {
|
|
28
|
+
this.startTime = 0;
|
|
29
|
+
this.endTime = 0;
|
|
30
|
+
this.running = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static measure<T>(fn: () => T | Promise<T>): Promise<{ result: T; elapsed_ms: number }> {
|
|
34
|
+
return (async () => {
|
|
35
|
+
const timer = new Timer();
|
|
36
|
+
timer.start();
|
|
37
|
+
const result = await fn();
|
|
38
|
+
const elapsed = timer.stop();
|
|
39
|
+
return { result, elapsed_ms: elapsed };
|
|
40
|
+
})();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static async measureAsync<T>(fn: () => Promise<T>): Promise<{ result: T; elapsed_ms: number }> {
|
|
44
|
+
const timer = new Timer();
|
|
45
|
+
timer.start();
|
|
46
|
+
const result = await fn();
|
|
47
|
+
const elapsed = timer.stop();
|
|
48
|
+
return { result, elapsed_ms: elapsed };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function median(values: number[]): number {
|
|
53
|
+
if (values.length === 0) return 0;
|
|
54
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
55
|
+
const mid = Math.floor(sorted.length / 2);
|
|
56
|
+
return sorted.length % 2 === 0
|
|
57
|
+
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
58
|
+
: sorted[mid];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function mean(values: number[]): number {
|
|
62
|
+
if (values.length === 0) return 0;
|
|
63
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function p95(values: number[]): number {
|
|
67
|
+
if (values.length === 0) return 0;
|
|
68
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
69
|
+
const index = Math.ceil(sorted.length * 0.95) - 1;
|
|
70
|
+
return sorted[index];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function sum(values: number[]): number {
|
|
74
|
+
return values.reduce((a, b) => a + b, 0);
|
|
75
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { bench, BenchResult } from '../src/commands/bench';
|
|
3
|
+
|
|
4
|
+
describe('bench', () => {
|
|
5
|
+
it('should be a function', () => {
|
|
6
|
+
expect(typeof bench).toBe('function');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should accept options parameter', () => {
|
|
10
|
+
// Just verify the function accepts the expected parameters
|
|
11
|
+
expect(bench).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { loadConfig, ProviderConfig } from '../src/client';
|
|
3
|
+
|
|
4
|
+
describe('client', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
delete process.env.AI_XRAY_API_KEY;
|
|
7
|
+
delete process.env.AI_XRAY_BASE_URL;
|
|
8
|
+
delete process.env.AI_XRAY_MODEL;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should use default values when no env vars set', () => {
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
expect(config.baseUrl).toBe('https://api.openai.com/v1');
|
|
14
|
+
expect(config.model).toBe('gpt-4o');
|
|
15
|
+
expect(config.apiKey).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should use environment variables when set', () => {
|
|
19
|
+
process.env.AI_XRAY_API_KEY = 'test-key';
|
|
20
|
+
process.env.AI_XRAY_BASE_URL = 'https://custom.api.com/v1';
|
|
21
|
+
process.env.AI_XRAY_MODEL = 'gpt-5';
|
|
22
|
+
|
|
23
|
+
const config = loadConfig();
|
|
24
|
+
expect(config.apiKey).toBe('test-key');
|
|
25
|
+
expect(config.baseUrl).toBe('https://custom.api.com/v1');
|
|
26
|
+
expect(config.model).toBe('gpt-5');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should use provider config when specified', () => {
|
|
30
|
+
// This test requires a config file, so we just test the function exists
|
|
31
|
+
expect(loadConfig).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should loadConfig be a function', () => {
|
|
35
|
+
expect(typeof loadConfig).toBe('function');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { compare, CompareResult } from '../src/commands/compare';
|
|
3
|
+
|
|
4
|
+
describe('compare', () => {
|
|
5
|
+
it('should be a function', () => {
|
|
6
|
+
expect(typeof compare).toBe('function');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should return result with expected structure', async () => {
|
|
10
|
+
const result = await compare(['openai'], { prompt: 'Say hi' });
|
|
11
|
+
|
|
12
|
+
expect(result).toHaveProperty('prompt');
|
|
13
|
+
expect(result).toHaveProperty('results');
|
|
14
|
+
|
|
15
|
+
expect(result.prompt).toBe('Say hi');
|
|
16
|
+
expect(Array.isArray(result.results)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should accept custom prompt', async () => {
|
|
20
|
+
const customPrompt = 'Write a haiku';
|
|
21
|
+
const result = await compare(['openai'], { prompt: customPrompt });
|
|
22
|
+
expect(result.prompt).toBe(customPrompt);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { request } from '../src/utils/http';
|
|
3
|
+
|
|
4
|
+
describe('http', () => {
|
|
5
|
+
it('should be defined', () => {
|
|
6
|
+
expect(request).toBeDefined();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should be a function', () => {
|
|
10
|
+
expect(typeof request).toBe('function');
|
|
11
|
+
});
|
|
12
|
+
});
|
package/tests/id.test.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { identify } from '../src/commands/id';
|
|
3
|
+
|
|
4
|
+
describe('id', () => {
|
|
5
|
+
it('should be a function', () => {
|
|
6
|
+
expect(typeof identify).toBe('function');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should accept config parameter', () => {
|
|
10
|
+
// Just verify the function exists and accepts expected params
|
|
11
|
+
expect(identify).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ping } from '../src/commands/ping';
|
|
3
|
+
|
|
4
|
+
describe('ping', () => {
|
|
5
|
+
it('should be a function', () => {
|
|
6
|
+
expect(typeof ping).toBe('function');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should accept config parameter', () => {
|
|
10
|
+
expect(ping).toBeDefined();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { probe, ProbeResult } from '../src/commands/probe';
|
|
3
|
+
|
|
4
|
+
describe('probe', () => {
|
|
5
|
+
it('should be a function', () => {
|
|
6
|
+
expect(typeof probe).toBe('function');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should accept config parameter', () => {
|
|
10
|
+
// Just verify the function exists and accepts expected params
|
|
11
|
+
expect(probe).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { countTokens } from '../src/commands/tokenize';
|
|
3
|
+
|
|
4
|
+
describe('tokenize', () => {
|
|
5
|
+
it('should be a function', () => {
|
|
6
|
+
expect(typeof countTokens).toBe('function');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should count characters accurately', async () => {
|
|
10
|
+
const result = await countTokens('Hello world');
|
|
11
|
+
expect(result.characters).toBe(11);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should count words accurately', async () => {
|
|
15
|
+
const result = await countTokens('Hello world from AI');
|
|
16
|
+
expect(result.words).toBe(4);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should estimate tokens within reasonable range', async () => {
|
|
20
|
+
const text = 'The quick brown fox jumps over the lazy dog';
|
|
21
|
+
const result = await countTokens(text);
|
|
22
|
+
expect(result.estimated_tokens).toBeGreaterThan(0);
|
|
23
|
+
expect(result.estimated_tokens).toBeLessThan(50);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should compute cost estimate for known models', async () => {
|
|
27
|
+
const result = await countTokens('Hello world', { model: 'gpt-4o' });
|
|
28
|
+
expect(result.cost_estimate).toBeDefined();
|
|
29
|
+
expect(result.cost_estimate?.model).toBe('gpt-4o');
|
|
30
|
+
expect(result.cost_estimate?.input_cost_usd).toBeGreaterThan(0);
|
|
31
|
+
});
|
|
32
|
+
});
|
package/tsup.config.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { defineConfig } from 'tsup';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
entry: ['src/cli.ts'],
|
|
5
|
-
format: ['cjs'],
|
|
6
|
-
target: 'node18',
|
|
7
|
-
clean: true,
|
|
8
|
-
|
|
9
|
-
// We bundle everything so there are zero dependencies at runtime
|
|
10
|
-
noExternal: [/(.*)/],
|
|
11
|
-
});
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/cli.ts'],
|
|
5
|
+
format: ['cjs'],
|
|
6
|
+
target: 'node18',
|
|
7
|
+
clean: true,
|
|
8
|
+
sourcemap: true,
|
|
9
|
+
// We bundle everything so there are zero dependencies at runtime
|
|
10
|
+
noExternal: [/(.*)/],
|
|
11
|
+
});
|
package/vitest.config.ts
ADDED