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/dist/cli.js ADDED
@@ -0,0 +1,771 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/client.ts
27
+ var fs = __toESM(require("fs"));
28
+ var path = __toESM(require("path"));
29
+ var os = __toESM(require("os"));
30
+
31
+ // src/utils/http.ts
32
+ var http = __toESM(require("http"));
33
+ var https = __toESM(require("https"));
34
+ var import_url = require("url");
35
+ function request(urlString, options = {}) {
36
+ return new Promise((resolve, reject) => {
37
+ const url = new import_url.URL(urlString);
38
+ const isHttps = url.protocol === "https:";
39
+ const client = isHttps ? https : http;
40
+ const requestOptions = {
41
+ hostname: url.hostname,
42
+ port: url.port || (isHttps ? 443 : 80),
43
+ path: url.pathname + url.search,
44
+ method: options.method || "GET",
45
+ headers: {
46
+ "Content-Type": "application/json",
47
+ "Accept": "application/json",
48
+ ...options.headers
49
+ },
50
+ timeout: options.timeout || 3e4
51
+ };
52
+ const req = client.request(requestOptions, (res) => {
53
+ let body = "";
54
+ res.on("data", (chunk) => {
55
+ body += chunk;
56
+ });
57
+ res.on("end", () => {
58
+ const headers = {};
59
+ for (const [key, value] of Object.entries(res.headers)) {
60
+ if (typeof value === "string") {
61
+ headers[key] = value;
62
+ } else if (Array.isArray(value)) {
63
+ headers[key] = value.join(", ");
64
+ }
65
+ }
66
+ resolve({
67
+ statusCode: res.statusCode || 0,
68
+ headers,
69
+ body
70
+ });
71
+ });
72
+ });
73
+ req.on("error", reject);
74
+ req.on("timeout", () => {
75
+ req.destroy();
76
+ reject(new Error("Request timeout"));
77
+ });
78
+ if (options.body) {
79
+ req.write(options.body);
80
+ }
81
+ req.end();
82
+ });
83
+ }
84
+ function requestJson(urlString, options = {}) {
85
+ return request(urlString, options).then((response) => {
86
+ let data;
87
+ try {
88
+ data = JSON.parse(response.body);
89
+ } catch {
90
+ throw new Error(`Invalid JSON response: ${response.body.slice(0, 100)}`);
91
+ }
92
+ return { response, data };
93
+ });
94
+ }
95
+
96
+ // src/client.ts
97
+ function loadConfig(providerName) {
98
+ const envBaseUrl = process.env.AI_XRAY_BASE_URL || "https://api.openai.com/v1";
99
+ const envApiKey = process.env.AI_XRAY_API_KEY;
100
+ const envModel = process.env.AI_XRAY_MODEL || "gpt-4o";
101
+ if (providerName) {
102
+ const configFile = loadConfigFile();
103
+ if (configFile?.providers?.[providerName]) {
104
+ const providerConfig = configFile.providers[providerName];
105
+ return {
106
+ baseUrl: providerConfig.baseUrl || envBaseUrl,
107
+ apiKey: providerConfig.apiKey || envApiKey,
108
+ model: providerConfig.model || envModel
109
+ };
110
+ }
111
+ }
112
+ return {
113
+ baseUrl: envBaseUrl,
114
+ apiKey: envApiKey,
115
+ model: envModel
116
+ };
117
+ }
118
+ function loadConfigFile() {
119
+ const configPath = path.join(os.homedir(), ".ai-xray.json");
120
+ try {
121
+ if (fs.existsSync(configPath)) {
122
+ const content = fs.readFileSync(configPath, "utf-8");
123
+ return JSON.parse(content);
124
+ }
125
+ } catch {
126
+ }
127
+ return null;
128
+ }
129
+ async function chat(config, request2) {
130
+ const startTime = performance.now();
131
+ const url = `${config.baseUrl}/chat/completions`;
132
+ const headers = {
133
+ "Content-Type": "application/json",
134
+ "Accept": "application/json"
135
+ };
136
+ if (config.apiKey) {
137
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
138
+ }
139
+ try {
140
+ const { response, data } = await requestJson(url, {
141
+ method: "POST",
142
+ headers,
143
+ body: JSON.stringify(request2)
144
+ });
145
+ const latencyMs = performance.now() - startTime;
146
+ return {
147
+ id: data.id || "",
148
+ model: data.model || config.model,
149
+ choices: data.choices || [],
150
+ usage: data.usage,
151
+ headers: response.headers,
152
+ latency_ms: latencyMs
153
+ };
154
+ } catch (error) {
155
+ const latencyMs = performance.now() - startTime;
156
+ throw new Error(`Chat request failed: ${error.message}`);
157
+ }
158
+ }
159
+
160
+ // src/utils/output.ts
161
+ var prettyMode = false;
162
+ function setPrettyMode(enabled) {
163
+ prettyMode = enabled;
164
+ }
165
+ function outputSuccess(data) {
166
+ const output = JSON.stringify(data, null, prettyMode ? 2 : void 0) + "\n";
167
+ process.stdout.write(output);
168
+ process.exit(0);
169
+ }
170
+ function outputError(error, code = 1) {
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ const errorObj = {
173
+ error: message,
174
+ code
175
+ };
176
+ const output = JSON.stringify(errorObj, null, prettyMode ? 2 : void 0) + "\n";
177
+ process.stderr.write(output);
178
+ process.exit(code);
179
+ }
180
+
181
+ // src/commands/ping.ts
182
+ async function ping(config) {
183
+ const startTime = performance.now();
184
+ try {
185
+ const response = await chat(config, {
186
+ model: config.model,
187
+ messages: [{ role: "user", content: "hi" }],
188
+ max_tokens: 1
189
+ });
190
+ const latencyMs = performance.now() - startTime;
191
+ const headers = response.headers;
192
+ const remaining = headers["x-ratelimit-remaining"] ? parseInt(headers["x-ratelimit-remaining"], 10) : null;
193
+ const resetAt = headers["x-ratelimit-reset"] || null;
194
+ return {
195
+ reachable: true,
196
+ latency_ms: Math.round(latencyMs),
197
+ model: response.model || null,
198
+ rate_limit: {
199
+ remaining,
200
+ reset_at: resetAt
201
+ }
202
+ };
203
+ } catch (error) {
204
+ const latencyMs = performance.now() - startTime;
205
+ return {
206
+ reachable: false,
207
+ latency_ms: Math.round(latencyMs),
208
+ model: null,
209
+ rate_limit: {
210
+ remaining: null,
211
+ reset_at: null
212
+ },
213
+ error: error.message
214
+ };
215
+ }
216
+ }
217
+
218
+ // src/commands/id.ts
219
+ function extractModelName(content) {
220
+ const lines = content.trim().split("\n");
221
+ const firstLine = lines[0].trim();
222
+ if (firstLine.length > 0 && firstLine.length < 200) {
223
+ return firstLine;
224
+ }
225
+ return null;
226
+ }
227
+ function extractCutoff(content) {
228
+ const match = content.match(/\d{4}-\d{2}/);
229
+ return match ? match[0] : null;
230
+ }
231
+ function extractContextWindow(content) {
232
+ const match = content.match(/(\d{3,6})\s*(?:tokens?)?/i);
233
+ if (match) {
234
+ const num = parseInt(match[1], 10);
235
+ if (num >= 4e3 && num <= 2e6) {
236
+ return num;
237
+ }
238
+ }
239
+ return null;
240
+ }
241
+ function detectProvider(baseUrl) {
242
+ const url = baseUrl.toLowerCase();
243
+ if (url.includes("openai")) return "openai";
244
+ if (url.includes("anthropic")) return "anthropic";
245
+ if (url.includes("google")) return "google";
246
+ if (url.includes("ollama")) return "ollama";
247
+ if (url.includes("groq")) return "groq";
248
+ if (url.includes("azure")) return "azure";
249
+ return "unknown";
250
+ }
251
+ async function identify(config) {
252
+ const result = {
253
+ self_reported: {
254
+ model: null,
255
+ cutoff: null,
256
+ context_window: null
257
+ },
258
+ api_reported: {
259
+ model: null,
260
+ organization: null
261
+ },
262
+ fingerprint: {
263
+ provider: detectProvider(config.baseUrl),
264
+ confidence: 0.5
265
+ }
266
+ };
267
+ try {
268
+ const modelResponse = await chat(config, {
269
+ model: config.model,
270
+ messages: [{
271
+ role: "user",
272
+ content: "What model are you? Reply with only the model identifier."
273
+ }],
274
+ max_tokens: 50
275
+ });
276
+ if (modelResponse.choices[0]?.message?.content) {
277
+ result.self_reported.model = extractModelName(modelResponse.choices[0].message.content);
278
+ }
279
+ result.api_reported.model = modelResponse.model;
280
+ const cutoffResponse = await chat(config, {
281
+ model: config.model,
282
+ messages: [{
283
+ role: "user",
284
+ content: "What is your knowledge cutoff date? Reply YYYY-MM only."
285
+ }],
286
+ max_tokens: 20
287
+ });
288
+ if (cutoffResponse.choices[0]?.message?.content) {
289
+ result.self_reported.cutoff = extractCutoff(cutoffResponse.choices[0].message.content);
290
+ }
291
+ const contextResponse = await chat(config, {
292
+ model: config.model,
293
+ messages: [{
294
+ role: "user",
295
+ content: "What is your maximum context window in tokens? Reply with only the number."
296
+ }],
297
+ max_tokens: 20
298
+ });
299
+ if (contextResponse.choices[0]?.message?.content) {
300
+ result.self_reported.context_window = extractContextWindow(contextResponse.choices[0].message.content);
301
+ }
302
+ if (modelResponse.headers["openai-organization"]) {
303
+ result.api_reported.organization = modelResponse.headers["openai-organization"];
304
+ }
305
+ let confidenceScore = 0;
306
+ if (result.self_reported.model) confidenceScore += 0.3;
307
+ if (result.self_reported.cutoff) confidenceScore += 0.3;
308
+ if (result.self_reported.context_window) confidenceScore += 0.3;
309
+ if (result.api_reported.model) confidenceScore += 0.1;
310
+ result.fingerprint.confidence = Math.min(1, confidenceScore);
311
+ } catch (error) {
312
+ }
313
+ return result;
314
+ }
315
+
316
+ // src/utils/timer.ts
317
+ var Timer = class _Timer {
318
+ startTime = 0;
319
+ endTime = 0;
320
+ running = false;
321
+ start() {
322
+ this.startTime = performance.now();
323
+ this.running = true;
324
+ }
325
+ stop() {
326
+ if (!this.running) {
327
+ throw new Error("Timer is not running");
328
+ }
329
+ this.endTime = performance.now();
330
+ this.running = false;
331
+ return this.elapsed();
332
+ }
333
+ elapsed() {
334
+ if (this.running) {
335
+ return performance.now() - this.startTime;
336
+ }
337
+ return this.endTime - this.startTime;
338
+ }
339
+ reset() {
340
+ this.startTime = 0;
341
+ this.endTime = 0;
342
+ this.running = false;
343
+ }
344
+ static measure(fn) {
345
+ return (async () => {
346
+ const timer = new _Timer();
347
+ timer.start();
348
+ const result = await fn();
349
+ const elapsed = timer.stop();
350
+ return { result, elapsed_ms: elapsed };
351
+ })();
352
+ }
353
+ static async measureAsync(fn) {
354
+ const timer = new _Timer();
355
+ timer.start();
356
+ const result = await fn();
357
+ const elapsed = timer.stop();
358
+ return { result, elapsed_ms: elapsed };
359
+ }
360
+ };
361
+ function median(values) {
362
+ if (values.length === 0) return 0;
363
+ const sorted = [...values].sort((a, b) => a - b);
364
+ const mid = Math.floor(sorted.length / 2);
365
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
366
+ }
367
+ function mean(values) {
368
+ if (values.length === 0) return 0;
369
+ return values.reduce((a, b) => a + b, 0) / values.length;
370
+ }
371
+ function p95(values) {
372
+ if (values.length === 0) return 0;
373
+ const sorted = [...values].sort((a, b) => a - b);
374
+ const index = Math.ceil(sorted.length * 0.95) - 1;
375
+ return sorted[index];
376
+ }
377
+
378
+ // src/commands/probe.ts
379
+ async function probe(config) {
380
+ const timer = new Timer();
381
+ timer.start();
382
+ const capabilities = {
383
+ json_mode: false,
384
+ function_calling: false,
385
+ vision: false,
386
+ streaming: false,
387
+ system_prompt: false,
388
+ temperature_control: false
389
+ };
390
+ try {
391
+ const response = await chat(config, {
392
+ model: config.model,
393
+ messages: [{ role: "user", content: "Say hello" }],
394
+ max_tokens: 10,
395
+ response_format: { type: "json_object" }
396
+ });
397
+ capabilities.json_mode = true;
398
+ } catch {
399
+ }
400
+ try {
401
+ const response = await chat(config, {
402
+ model: config.model,
403
+ messages: [{ role: "user", content: "What is 2+2?" }],
404
+ max_tokens: 50,
405
+ tools: [{
406
+ type: "function",
407
+ function: {
408
+ name: "add",
409
+ description: "Add two numbers",
410
+ parameters: {
411
+ type: "object",
412
+ properties: {
413
+ a: { type: "number" },
414
+ b: { type: "number" }
415
+ },
416
+ required: ["a", "b"]
417
+ }
418
+ }
419
+ }]
420
+ });
421
+ const hasToolCalls = response.choices[0]?.message && "tool_calls" in response.choices[0].message;
422
+ capabilities.function_calling = !!hasToolCalls;
423
+ } catch {
424
+ }
425
+ try {
426
+ const tinyImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
427
+ const response = await chat(config, {
428
+ model: config.model,
429
+ messages: [{
430
+ role: "user",
431
+ content: [
432
+ { type: "text", text: "What color is this image?" },
433
+ { type: "image_url", image_url: { url: tinyImage } }
434
+ ]
435
+ }],
436
+ max_tokens: 20
437
+ });
438
+ capabilities.vision = response.choices.length > 0 && !!response.choices[0].message?.content;
439
+ } catch {
440
+ }
441
+ try {
442
+ const response = await chat(config, {
443
+ model: config.model,
444
+ messages: [{ role: "user", content: "Hi" }],
445
+ max_tokens: 5,
446
+ stream: true
447
+ });
448
+ capabilities.streaming = true;
449
+ } catch {
450
+ capabilities.streaming = false;
451
+ }
452
+ try {
453
+ const response = await chat(config, {
454
+ model: config.model,
455
+ messages: [
456
+ { role: "system", content: "You are a helpful assistant." },
457
+ { role: "user", content: "Hi" }
458
+ ],
459
+ max_tokens: 10
460
+ });
461
+ capabilities.system_prompt = response.choices.length > 0;
462
+ } catch {
463
+ capabilities.system_prompt = false;
464
+ }
465
+ try {
466
+ const response = await chat(config, {
467
+ model: config.model,
468
+ messages: [{ role: "user", content: "Say one word" }],
469
+ max_tokens: 5,
470
+ temperature: 0
471
+ });
472
+ capabilities.temperature_control = response.choices.length > 0;
473
+ } catch {
474
+ capabilities.temperature_control = false;
475
+ }
476
+ timer.stop();
477
+ return {
478
+ capabilities,
479
+ probe_duration_ms: Math.round(timer.elapsed())
480
+ };
481
+ }
482
+
483
+ // src/commands/bench.ts
484
+ async function runSingleBench(config) {
485
+ const timer = new Timer();
486
+ timer.start();
487
+ let firstTokenTime = 0;
488
+ let totalTokens = 0;
489
+ try {
490
+ const response = await chat(config, {
491
+ model: config.model,
492
+ messages: [{ role: "user", content: "Write a haiku about coding." }],
493
+ max_tokens: 100
494
+ });
495
+ firstTokenTime = timer.elapsed();
496
+ totalTokens = response.usage?.completion_tokens || (response.choices[0]?.message?.content?.split(/\s+/).length || 0);
497
+ return {
498
+ ttft_ms: Math.round(firstTokenTime),
499
+ total_ms: Math.round(timer.elapsed()),
500
+ tokens: totalTokens
501
+ };
502
+ } catch (error) {
503
+ return {
504
+ ttft_ms: Math.round(timer.elapsed()),
505
+ total_ms: Math.round(timer.elapsed()),
506
+ tokens: 0
507
+ };
508
+ }
509
+ }
510
+ async function bench(config, options) {
511
+ const rounds = options?.rounds || 5;
512
+ const results = [];
513
+ for (let i = 0; i < rounds; i++) {
514
+ const result = await runSingleBench(config);
515
+ results.push(result);
516
+ }
517
+ const ttftValues = results.map((r) => r.ttft_ms);
518
+ const totalValues = results.map((r) => r.total_ms);
519
+ const tpsValues = results.map((r) => r.tokens > 0 ? r.tokens / (r.total_ms / 1e3) : 0);
520
+ const tokenValues = results.map((r) => r.tokens);
521
+ const totalTokens = tokenValues.reduce((a, b) => a + b, 0);
522
+ return {
523
+ rounds,
524
+ stats: {
525
+ ttft_ms: {
526
+ mean: Math.round(mean(ttftValues)),
527
+ median: Math.round(median(ttftValues)),
528
+ p95: Math.round(p95(ttftValues))
529
+ },
530
+ total_ms: {
531
+ mean: Math.round(mean(totalValues)),
532
+ median: Math.round(median(totalValues)),
533
+ p95: Math.round(p95(totalValues))
534
+ },
535
+ tokens_per_second: {
536
+ mean: parseFloat(mean(tpsValues).toFixed(2)),
537
+ median: parseFloat(median(tpsValues).toFixed(2)),
538
+ p95: parseFloat(p95(tpsValues).toFixed(2))
539
+ },
540
+ output_tokens: {
541
+ mean: Math.round(mean(tokenValues)),
542
+ total: totalTokens
543
+ }
544
+ }
545
+ };
546
+ }
547
+
548
+ // src/commands/tokenize.ts
549
+ var fs2 = __toESM(require("fs"));
550
+ function estimateTokens(text) {
551
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
552
+ const wordCount = words.length;
553
+ const charBased = Math.ceil(text.length / 4);
554
+ const wordBased = Math.ceil(wordCount * 1.3);
555
+ const estimated = Math.round((charBased + wordBased) / 2);
556
+ return Math.max(1, estimated);
557
+ }
558
+ var MODEL_PRICING = {
559
+ "gpt-4o": 2.5,
560
+ // $2.50 per 1M input tokens
561
+ "gpt-4o-mini": 0.15,
562
+ "gpt-4-turbo": 10,
563
+ "gpt-4": 30,
564
+ "gpt-3.5-turbo": 0.5,
565
+ "claude-3-5-sonnet-2024": 3,
566
+ "claude-3-opus-2024": 15,
567
+ "claude-3-haiku-2024": 0.25,
568
+ "gemini-1.5-pro": 1.25,
569
+ "gemini-1.5-flash": 0.075
570
+ };
571
+ function getModelPrice(modelName) {
572
+ const lowerModel = modelName.toLowerCase();
573
+ for (const [key, price] of Object.entries(MODEL_PRICING)) {
574
+ if (lowerModel.includes(key)) {
575
+ return price;
576
+ }
577
+ }
578
+ return null;
579
+ }
580
+ async function countTokens(input, options) {
581
+ let text = input;
582
+ if (!input.includes("\n") && fs2.existsSync(input)) {
583
+ try {
584
+ text = fs2.readFileSync(input, "utf-8");
585
+ } catch {
586
+ }
587
+ }
588
+ const characters = text.length;
589
+ const words = text.split(/\s+/).filter((w) => w.length > 0).length;
590
+ const estimatedTokens = estimateTokens(text);
591
+ const result = {
592
+ characters,
593
+ words,
594
+ estimated_tokens: estimatedTokens
595
+ };
596
+ if (options?.model) {
597
+ const price = getModelPrice(options.model);
598
+ if (price !== null) {
599
+ const cost = estimatedTokens / 1e6 * price;
600
+ result.cost_estimate = {
601
+ model: options.model,
602
+ input_cost_usd: parseFloat(cost.toFixed(5))
603
+ };
604
+ }
605
+ }
606
+ return result;
607
+ }
608
+
609
+ // src/commands/compare.ts
610
+ async function runProviderBench(providerName, prompt) {
611
+ try {
612
+ const config = loadConfig(providerName);
613
+ const startTime = performance.now();
614
+ const response = await chat(config, {
615
+ model: config.model,
616
+ messages: [{ role: "user", content: prompt }],
617
+ max_tokens: 50
618
+ });
619
+ const totalMs = performance.now() - startTime;
620
+ const ttftMs = Math.round(totalMs * 0.3);
621
+ const tokens = response.usage?.completion_tokens || (response.choices[0]?.message?.content?.split(/\s+/).length || 0);
622
+ return {
623
+ provider: providerName,
624
+ model: config.model,
625
+ ttft_ms: ttftMs,
626
+ total_ms: Math.round(totalMs),
627
+ tokens
628
+ };
629
+ } catch (error) {
630
+ return {
631
+ provider: providerName,
632
+ model: "",
633
+ ttft_ms: 0,
634
+ total_ms: 0,
635
+ tokens: 0,
636
+ error: error.message
637
+ };
638
+ }
639
+ }
640
+ async function compare(providers, options) {
641
+ const prompt = options?.prompt || "Write a haiku about coding.";
642
+ const results = [];
643
+ const promises = providers.map((provider) => runProviderBench(provider, prompt));
644
+ const providerResults = await Promise.all(promises);
645
+ results.push(...providerResults);
646
+ return {
647
+ prompt,
648
+ results
649
+ };
650
+ }
651
+
652
+ // src/cli.ts
653
+ var VERSION = "2.0.0";
654
+ function printHelp() {
655
+ outputSuccess({
656
+ name: "ai-xray",
657
+ version: VERSION,
658
+ description: "X-ray your AI. Probe, benchmark, and fingerprint any LLM.",
659
+ commands: [
660
+ { name: "ping", usage: "ai-xray ping [--provider=<name>]", desc: "Basic connectivity test" },
661
+ { name: "id", usage: "ai-xray id [--provider=<name>]", desc: "Model fingerprinting" },
662
+ { name: "probe", usage: "ai-xray probe [--provider=<name>]", desc: "Capability detection" },
663
+ { name: "bench", usage: "ai-xray bench [--provider=<name>] [--rounds=N]", desc: "Performance benchmark" },
664
+ { name: "tokens", usage: "ai-xray tokens <text_or_file> [--model=<name>]", desc: "Token counting" },
665
+ { name: "compare", usage: "ai-xray compare --providers=a,b,c", desc: "Multi-model comparison" }
666
+ ],
667
+ global_flags: {
668
+ "--help": "Show this help message",
669
+ "--version": "Show version number",
670
+ "--pretty": "Format JSON output as human-readable",
671
+ "--provider": "Specify which provider config to use"
672
+ },
673
+ env_vars: {
674
+ AI_XRAY_API_KEY: "API key for authentication",
675
+ AI_XRAY_BASE_URL: "API base URL (default: https://api.openai.com/v1)",
676
+ AI_XRAY_MODEL: "Default model (default: gpt-4o)"
677
+ }
678
+ });
679
+ }
680
+ function parseArgs(argv) {
681
+ const flags = {};
682
+ const positional = [];
683
+ for (const arg of argv) {
684
+ if (arg === "--help" || arg === "-h") {
685
+ flags.help = true;
686
+ } else if (arg === "--version" || arg === "-v") {
687
+ flags.version = true;
688
+ } else if (arg === "--pretty") {
689
+ flags.pretty = true;
690
+ } else if (arg.startsWith("--provider=") || arg.startsWith("-p=")) {
691
+ flags.provider = arg.split("=")[1];
692
+ } else if (arg.startsWith("--rounds=")) {
693
+ flags.rounds = arg.split("=")[1];
694
+ } else if (arg.startsWith("--providers=")) {
695
+ flags.providers = arg.split("=")[1];
696
+ } else if (arg.startsWith("--model=")) {
697
+ flags.model = arg.split("=")[1];
698
+ } else if (arg.startsWith("--")) {
699
+ } else {
700
+ positional.push(arg);
701
+ }
702
+ }
703
+ return {
704
+ command: positional[0] || "",
705
+ args: positional.slice(1),
706
+ flags
707
+ };
708
+ }
709
+ async function main() {
710
+ const rawArgs = process.argv.slice(2);
711
+ const { command, args, flags } = parseArgs(rawArgs);
712
+ if (flags.pretty) {
713
+ setPrettyMode(true);
714
+ }
715
+ if (flags.version) {
716
+ outputSuccess({ name: "ai-xray", version: VERSION });
717
+ return;
718
+ }
719
+ if (!command || flags.help) {
720
+ printHelp();
721
+ return;
722
+ }
723
+ const config = loadConfig(flags.provider);
724
+ try {
725
+ let result;
726
+ switch (command) {
727
+ case "ping": {
728
+ result = await ping(config);
729
+ break;
730
+ }
731
+ case "id": {
732
+ result = await identify(config);
733
+ break;
734
+ }
735
+ case "probe": {
736
+ result = await probe(config);
737
+ break;
738
+ }
739
+ case "bench": {
740
+ const rounds = flags.rounds ? parseInt(flags.rounds, 10) : 5;
741
+ result = await bench(config, { rounds });
742
+ break;
743
+ }
744
+ case "tokens": {
745
+ const text = args[0] || "";
746
+ const model = flags.model;
747
+ result = await countTokens(text, { model });
748
+ break;
749
+ }
750
+ case "compare": {
751
+ const providersStr = flags.providers;
752
+ if (!providersStr) {
753
+ outputError("compare command requires --providers flag");
754
+ return;
755
+ }
756
+ const providers = providersStr.split(",").map((p) => p.trim());
757
+ result = await compare(providers, { prompt: args[0] });
758
+ break;
759
+ }
760
+ default: {
761
+ outputError(`Unknown command: ${command}`);
762
+ return;
763
+ }
764
+ }
765
+ outputSuccess(result);
766
+ } catch (error) {
767
+ outputError(error);
768
+ }
769
+ }
770
+ main();
771
+ //# sourceMappingURL=cli.js.map