vertex-ai-proxy 1.0.3 → 1.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/dist/cli.js CHANGED
@@ -1,65 +1,1167 @@
1
1
  #!/usr/bin/env node
2
- import { startServer } from './server.js';
3
- const args = process.argv.slice(2);
4
- const config = {};
5
- for (let i = 0; i < args.length; i++) {
6
- const arg = args[i];
7
- const next = args[i + 1];
8
- switch (arg) {
9
- case '--port':
10
- case '-p':
11
- config.port = parseInt(next);
12
- i++;
13
- break;
14
- case '--host':
15
- config.host = next;
16
- i++;
17
- break;
18
- case '--project':
19
- config.projectId = next;
20
- i++;
21
- break;
22
- case '--claude-regions':
23
- config.claudeRegions = next.split(',');
24
- i++;
25
- break;
26
- case '--gemini-location':
27
- config.geminiLocation = next;
28
- i++;
29
- break;
30
- case '--max-concurrent':
31
- config.maxConcurrent = parseInt(next);
32
- i++;
33
- break;
34
- case '--enable-logging':
35
- config.enableRequestLogging = true;
36
- break;
37
- case '--disable-cache':
38
- config.enablePromptCache = false;
39
- break;
40
- case '--disable-metrics':
41
- config.enableMetrics = false;
42
- break;
43
- case '-h':
44
- case '--help':
45
- console.log('Vertex AI Proxy - OpenAI-compatible API for Claude/Gemini on Vertex AI\n');
46
- console.log('Usage: vertex-proxy [options]\n');
47
- console.log('Options:');
48
- console.log(' -p, --port <port> Server port (default: 8001)');
49
- console.log(' --project <id> Google Cloud project ID');
50
- console.log(' --claude-regions <list> Comma-separated regions');
51
- console.log(' --max-concurrent <n> Max concurrent requests');
52
- console.log(' --enable-logging Enable request logging');
53
- console.log(' --disable-cache Disable prompt caching');
54
- console.log(' -h, --help Show help');
2
+ /**
3
+ * Vertex AI Proxy CLI
4
+ *
5
+ * Commands:
6
+ * vertex-ai-proxy Start the proxy server
7
+ * vertex-ai-proxy start Start as background daemon
8
+ * vertex-ai-proxy stop Stop the daemon
9
+ * vertex-ai-proxy restart Restart the daemon
10
+ * vertex-ai-proxy status Show proxy status
11
+ * vertex-ai-proxy logs Show proxy logs
12
+ * vertex-ai-proxy models List all available models
13
+ * vertex-ai-proxy models fetch Fetch/verify models from Vertex AI
14
+ * vertex-ai-proxy models info <model> Show detailed model info
15
+ * vertex-ai-proxy models enable <model> Enable a model
16
+ * vertex-ai-proxy config Show current config
17
+ * vertex-ai-proxy config set Interactive config setup
18
+ * vertex-ai-proxy config set-default Set default model
19
+ * vertex-ai-proxy config add-alias Add model alias
20
+ * vertex-ai-proxy config export Export for OpenClaw
21
+ * vertex-ai-proxy setup-openclaw Configure OpenClaw integration
22
+ * vertex-ai-proxy check Check Google Cloud setup
23
+ * vertex-ai-proxy install-service Install as systemd service
24
+ */
25
+ import { Command } from 'commander';
26
+ import chalk from 'chalk';
27
+ import ora from 'ora';
28
+ import { execSync, spawn } from 'child_process';
29
+ import * as fs from 'fs';
30
+ import * as path from 'path';
31
+ import * as os from 'os';
32
+ import * as yaml from 'js-yaml';
33
+ import * as readline from 'readline';
34
+ const VERSION = '1.1.0';
35
+ const CONFIG_DIR = path.join(os.homedir(), '.vertex-proxy');
36
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.yaml');
37
+ const DATA_DIR = path.join(os.homedir(), '.vertex_proxy');
38
+ const PID_FILE = path.join(DATA_DIR, 'proxy.pid');
39
+ const LOG_FILE = path.join(DATA_DIR, 'proxy.log');
40
+ const STATS_FILE = path.join(DATA_DIR, 'stats.json');
41
+ // ============================================================================
42
+ // Model Catalog
43
+ // ============================================================================
44
+ const MODEL_CATALOG = {
45
+ // Claude Models
46
+ 'claude-opus-4-5@20251101': {
47
+ id: 'claude-opus-4-5@20251101',
48
+ name: 'Claude Opus 4.5',
49
+ provider: 'anthropic',
50
+ description: 'Most capable Claude. Best for complex reasoning.',
51
+ contextWindow: 200000,
52
+ maxTokens: 8192,
53
+ inputPrice: 15,
54
+ outputPrice: 75,
55
+ regions: ['us-east5', 'europe-west1'],
56
+ capabilities: ['text', 'vision', 'tools', 'thinking']
57
+ },
58
+ 'claude-opus-4-1@20250410': {
59
+ id: 'claude-opus-4-1@20250410',
60
+ name: 'Claude Opus 4.1',
61
+ provider: 'anthropic',
62
+ description: 'Previous Opus generation.',
63
+ contextWindow: 200000,
64
+ maxTokens: 8192,
65
+ inputPrice: 15,
66
+ outputPrice: 75,
67
+ regions: ['us-east5', 'europe-west1'],
68
+ capabilities: ['text', 'vision', 'tools']
69
+ },
70
+ 'claude-sonnet-4-5@20250514': {
71
+ id: 'claude-sonnet-4-5@20250514',
72
+ name: 'Claude Sonnet 4.5',
73
+ provider: 'anthropic',
74
+ description: 'Balanced performance and cost.',
75
+ contextWindow: 200000,
76
+ maxTokens: 8192,
77
+ inputPrice: 3,
78
+ outputPrice: 15,
79
+ regions: ['us-east5', 'europe-west1'],
80
+ capabilities: ['text', 'vision', 'tools', 'thinking']
81
+ },
82
+ 'claude-sonnet-4@20250514': {
83
+ id: 'claude-sonnet-4@20250514',
84
+ name: 'Claude Sonnet 4',
85
+ provider: 'anthropic',
86
+ description: 'Previous Sonnet generation.',
87
+ contextWindow: 200000,
88
+ maxTokens: 8192,
89
+ inputPrice: 3,
90
+ outputPrice: 15,
91
+ regions: ['us-east5', 'europe-west1'],
92
+ capabilities: ['text', 'vision', 'tools']
93
+ },
94
+ 'claude-haiku-4-5@20251001': {
95
+ id: 'claude-haiku-4-5@20251001',
96
+ name: 'Claude Haiku 4.5',
97
+ provider: 'anthropic',
98
+ description: 'Fastest and most affordable.',
99
+ contextWindow: 200000,
100
+ maxTokens: 8192,
101
+ inputPrice: 0.25,
102
+ outputPrice: 1.25,
103
+ regions: ['us-east5', 'europe-west1'],
104
+ capabilities: ['text', 'vision', 'tools']
105
+ },
106
+ // Gemini Models
107
+ 'gemini-3-pro': {
108
+ id: 'gemini-3-pro',
109
+ name: 'Gemini 3 Pro',
110
+ provider: 'google',
111
+ description: 'Latest Gemini with multimodal.',
112
+ contextWindow: 1000000,
113
+ maxTokens: 8192,
114
+ inputPrice: 2.5,
115
+ outputPrice: 15,
116
+ regions: ['us-central1', 'europe-west4'],
117
+ capabilities: ['text', 'vision', 'audio', 'video', 'tools']
118
+ },
119
+ 'gemini-2.5-pro': {
120
+ id: 'gemini-2.5-pro',
121
+ name: 'Gemini 2.5 Pro',
122
+ provider: 'google',
123
+ description: 'Previous Gemini Pro.',
124
+ contextWindow: 1000000,
125
+ maxTokens: 8192,
126
+ inputPrice: 1.25,
127
+ outputPrice: 5,
128
+ regions: ['us-central1', 'europe-west4'],
129
+ capabilities: ['text', 'vision', 'tools']
130
+ },
131
+ 'gemini-2.5-flash': {
132
+ id: 'gemini-2.5-flash',
133
+ name: 'Gemini 2.5 Flash',
134
+ provider: 'google',
135
+ description: 'Fast and affordable Gemini.',
136
+ contextWindow: 1000000,
137
+ maxTokens: 8192,
138
+ inputPrice: 0.15,
139
+ outputPrice: 0.60,
140
+ regions: ['us-central1', 'europe-west4'],
141
+ capabilities: ['text', 'vision', 'tools']
142
+ },
143
+ 'gemini-2.5-flash-lite': {
144
+ id: 'gemini-2.5-flash-lite',
145
+ name: 'Gemini 2.5 Flash Lite',
146
+ provider: 'google',
147
+ description: 'Most affordable Gemini.',
148
+ contextWindow: 1000000,
149
+ maxTokens: 8192,
150
+ inputPrice: 0.075,
151
+ outputPrice: 0.30,
152
+ regions: ['us-central1', 'europe-west4'],
153
+ capabilities: ['text']
154
+ },
155
+ // Imagen Models
156
+ 'imagen-4.0-generate-001': {
157
+ id: 'imagen-4.0-generate-001',
158
+ name: 'Imagen 4 Generate',
159
+ provider: 'imagen',
160
+ description: 'Best quality image generation.',
161
+ contextWindow: 0,
162
+ maxTokens: 0,
163
+ inputPrice: 0.04,
164
+ outputPrice: 0,
165
+ regions: ['us-central1'],
166
+ capabilities: ['image-generation']
167
+ },
168
+ 'imagen-4.0-fast-generate-001': {
169
+ id: 'imagen-4.0-fast-generate-001',
170
+ name: 'Imagen 4 Fast',
171
+ provider: 'imagen',
172
+ description: 'Faster image generation.',
173
+ contextWindow: 0,
174
+ maxTokens: 0,
175
+ inputPrice: 0.02,
176
+ outputPrice: 0,
177
+ regions: ['us-central1'],
178
+ capabilities: ['image-generation']
179
+ },
180
+ 'imagen-4.0-ultra-generate-001': {
181
+ id: 'imagen-4.0-ultra-generate-001',
182
+ name: 'Imagen 4 Ultra',
183
+ provider: 'imagen',
184
+ description: 'Highest quality images.',
185
+ contextWindow: 0,
186
+ maxTokens: 0,
187
+ inputPrice: 0.08,
188
+ outputPrice: 0,
189
+ regions: ['us-central1'],
190
+ capabilities: ['image-generation']
191
+ }
192
+ };
193
+ // ============================================================================
194
+ // Helper Functions
195
+ // ============================================================================
196
+ function ensureDataDir() {
197
+ if (!fs.existsSync(DATA_DIR)) {
198
+ fs.mkdirSync(DATA_DIR, { recursive: true });
199
+ }
200
+ }
201
+ function loadConfig() {
202
+ const defaultConfig = {
203
+ project_id: process.env.GOOGLE_CLOUD_PROJECT || '',
204
+ default_region: 'us-east5',
205
+ google_region: 'us-central1',
206
+ model_aliases: {},
207
+ fallback_chains: {},
208
+ default_model: 'claude-sonnet-4-5@20250514',
209
+ enabled_models: [],
210
+ auto_truncate: true,
211
+ reserve_output_tokens: 4096
212
+ };
213
+ try {
214
+ if (fs.existsSync(CONFIG_FILE)) {
215
+ const content = fs.readFileSync(CONFIG_FILE, 'utf8');
216
+ const fileConfig = yaml.load(content);
217
+ return { ...defaultConfig, ...fileConfig };
218
+ }
219
+ }
220
+ catch (e) { }
221
+ return defaultConfig;
222
+ }
223
+ function saveConfig(config) {
224
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
225
+ const enabledModelsYaml = config.enabled_models.length > 0
226
+ ? config.enabled_models.map(m => ` - "${m}"`).join('\n')
227
+ : ' []';
228
+ const aliasesYaml = Object.keys(config.model_aliases).length > 0
229
+ ? Object.entries(config.model_aliases).map(([k, v]) => ` ${k}: "${v}"`).join('\n')
230
+ : ' {}';
231
+ const fallbacksYaml = Object.keys(config.fallback_chains).length > 0
232
+ ? Object.entries(config.fallback_chains).map(([k, v]) => ` "${k}":\n${v.map(m => ` - "${m}"`).join('\n')}`).join('\n')
233
+ : ' {}';
234
+ const yamlContent = `# Vertex AI Proxy Configuration
235
+ # Generated: ${new Date().toISOString()}
236
+
237
+ project_id: "${config.project_id}"
238
+ default_region: "${config.default_region}"
239
+ google_region: "${config.google_region}"
240
+
241
+ default_model: "${config.default_model}"
242
+
243
+ enabled_models:
244
+ ${enabledModelsYaml}
245
+
246
+ model_aliases:
247
+ ${aliasesYaml}
248
+
249
+ fallback_chains:
250
+ ${fallbacksYaml}
251
+
252
+ auto_truncate: ${config.auto_truncate}
253
+ reserve_output_tokens: ${config.reserve_output_tokens}
254
+ `;
255
+ fs.writeFileSync(CONFIG_FILE, yamlContent);
256
+ }
257
+ async function prompt(question) {
258
+ const rl = readline.createInterface({
259
+ input: process.stdin,
260
+ output: process.stdout
261
+ });
262
+ return new Promise((resolve) => {
263
+ rl.question(question, (answer) => {
264
+ rl.close();
265
+ resolve(answer.trim());
266
+ });
267
+ });
268
+ }
269
+ async function promptYesNo(question, defaultYes = true) {
270
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
271
+ const answer = await prompt(`${question} ${hint} `);
272
+ if (!answer)
273
+ return defaultYes;
274
+ return answer.toLowerCase().startsWith('y');
275
+ }
276
+ async function promptSelect(question, options) {
277
+ console.log(question);
278
+ options.forEach((opt, i) => console.log(chalk.gray(` ${i + 1}) ${opt}`)));
279
+ const answer = await prompt(chalk.cyan('Select (number): '));
280
+ const num = parseInt(answer);
281
+ if (isNaN(num) || num < 1 || num > options.length)
282
+ return 0;
283
+ return num - 1;
284
+ }
285
+ function formatPrice(input, output) {
286
+ if (input === 0 && output === 0)
287
+ return chalk.green('Per-image');
288
+ return chalk.yellow(`$${input}/$${output}`);
289
+ }
290
+ function formatCapabilities(caps) {
291
+ const icons = {
292
+ 'text': 'šŸ“', 'vision': 'šŸ‘ļø', 'audio': 'šŸŽµ', 'video': 'šŸŽ¬',
293
+ 'tools': 'šŸ”§', 'thinking': '🧠', 'image-generation': 'šŸŽØ', 'image-edit': 'āœļø'
294
+ };
295
+ return caps.map(c => icons[c] || c).join(' ');
296
+ }
297
+ function formatUptime(ms) {
298
+ const seconds = Math.floor(ms / 1000);
299
+ const minutes = Math.floor(seconds / 60);
300
+ const hours = Math.floor(minutes / 60);
301
+ const days = Math.floor(hours / 24);
302
+ if (days > 0)
303
+ return `${days}d ${hours % 24}h ${minutes % 60}m`;
304
+ if (hours > 0)
305
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
306
+ if (minutes > 0)
307
+ return `${minutes}m ${seconds % 60}s`;
308
+ return `${seconds}s`;
309
+ }
310
+ // ============================================================================
311
+ // Daemon Management
312
+ // ============================================================================
313
+ function getPid() {
314
+ try {
315
+ if (fs.existsSync(PID_FILE)) {
316
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
317
+ if (!isNaN(pid))
318
+ return pid;
319
+ }
320
+ }
321
+ catch (e) { }
322
+ return null;
323
+ }
324
+ function isRunning(pid) {
325
+ try {
326
+ process.kill(pid, 0);
327
+ return true;
328
+ }
329
+ catch (e) {
330
+ return false;
331
+ }
332
+ }
333
+ function loadStats() {
334
+ try {
335
+ if (fs.existsSync(STATS_FILE)) {
336
+ return JSON.parse(fs.readFileSync(STATS_FILE, 'utf8'));
337
+ }
338
+ }
339
+ catch (e) { }
340
+ return null;
341
+ }
342
+ async function startDaemon(options) {
343
+ console.log(chalk.blue.bold('\nšŸš€ Starting Vertex AI Proxy Daemon\n'));
344
+ const existingPid = getPid();
345
+ if (existingPid && isRunning(existingPid)) {
346
+ console.log(chalk.yellow(`āš ļø Proxy already running (PID: ${existingPid})`));
347
+ console.log(chalk.gray(' Use: vertex-ai-proxy restart'));
348
+ return;
349
+ }
350
+ const config = loadConfig();
351
+ let projectId = options.project || config.project_id || process.env.GOOGLE_CLOUD_PROJECT;
352
+ if (!projectId) {
353
+ console.log(chalk.red('Project ID required. Use --project or run config set'));
354
+ process.exit(1);
355
+ }
356
+ const port = options.port || '8001';
357
+ ensureDataDir();
358
+ // Build the command to run
359
+ const distPath = path.join(path.dirname(process.argv[1]), '..', 'dist', 'index.js');
360
+ const srcPath = path.join(path.dirname(process.argv[1]), '..', 'src', 'index.js');
361
+ let serverPath = fs.existsSync(distPath) ? distPath : srcPath;
362
+ // Spawn detached process
363
+ const env = {
364
+ ...process.env,
365
+ GOOGLE_CLOUD_PROJECT: projectId,
366
+ VERTEX_PROXY_PORT: port,
367
+ VERTEX_PROXY_REGION: options.region || config.default_region,
368
+ VERTEX_PROXY_GOOGLE_REGION: options.googleRegion || config.google_region
369
+ };
370
+ const logStream = fs.openSync(LOG_FILE, 'a');
371
+ const child = spawn(process.execPath, [serverPath], {
372
+ detached: true,
373
+ stdio: ['ignore', logStream, logStream],
374
+ env
375
+ });
376
+ // Write PID file
377
+ fs.writeFileSync(PID_FILE, child.pid.toString());
378
+ // Unref to allow parent to exit
379
+ child.unref();
380
+ console.log(chalk.green(`āœ“ Started daemon`));
381
+ console.log(chalk.gray(` PID: ${child.pid}`));
382
+ console.log(chalk.gray(` Port: ${port}`));
383
+ console.log(chalk.gray(` Logs: ${LOG_FILE}`));
384
+ console.log();
385
+ console.log(chalk.gray('Commands:'));
386
+ console.log(chalk.gray(' vertex-ai-proxy status - Check status'));
387
+ console.log(chalk.gray(' vertex-ai-proxy logs - View logs'));
388
+ console.log(chalk.gray(' vertex-ai-proxy stop - Stop daemon'));
389
+ }
390
+ async function stopDaemon() {
391
+ console.log(chalk.blue.bold('\nšŸ›‘ Stopping Vertex AI Proxy\n'));
392
+ const pid = getPid();
393
+ if (!pid) {
394
+ console.log(chalk.yellow('āš ļø No PID file found. Proxy may not be running.'));
395
+ return;
396
+ }
397
+ if (!isRunning(pid)) {
398
+ console.log(chalk.yellow(`āš ļø Process ${pid} not running. Cleaning up PID file.`));
399
+ fs.unlinkSync(PID_FILE);
400
+ return;
401
+ }
402
+ try {
403
+ process.kill(pid, 'SIGTERM');
404
+ console.log(chalk.green(`āœ“ Sent SIGTERM to PID ${pid}`));
405
+ // Wait for process to exit
406
+ let attempts = 0;
407
+ while (isRunning(pid) && attempts < 10) {
408
+ await new Promise(resolve => setTimeout(resolve, 500));
409
+ attempts++;
410
+ }
411
+ if (isRunning(pid)) {
412
+ console.log(chalk.yellow(' Process still running, sending SIGKILL...'));
413
+ process.kill(pid, 'SIGKILL');
414
+ }
415
+ // Clean up PID file
416
+ if (fs.existsSync(PID_FILE)) {
417
+ fs.unlinkSync(PID_FILE);
418
+ }
419
+ console.log(chalk.green('āœ“ Daemon stopped'));
420
+ }
421
+ catch (e) {
422
+ console.log(chalk.red(`Error stopping daemon: ${e.message}`));
423
+ }
424
+ }
425
+ async function restartDaemon(options) {
426
+ console.log(chalk.blue.bold('\nšŸ”„ Restarting Vertex AI Proxy\n'));
427
+ const pid = getPid();
428
+ if (pid && isRunning(pid)) {
429
+ await stopDaemon();
430
+ // Wait a moment for port to free
431
+ await new Promise(resolve => setTimeout(resolve, 1000));
432
+ }
433
+ await startDaemon(options);
434
+ }
435
+ async function showStatus() {
436
+ console.log(chalk.blue.bold('\nšŸ“Š Vertex AI Proxy Status\n'));
437
+ const pid = getPid();
438
+ const stats = loadStats();
439
+ const config = loadConfig();
440
+ // Process status
441
+ console.log(chalk.cyan('Process:'));
442
+ if (pid && isRunning(pid)) {
443
+ console.log(chalk.green(` āœ“ Running (PID: ${pid})`));
444
+ }
445
+ else if (pid) {
446
+ console.log(chalk.red(` āœ— Not running (stale PID: ${pid})`));
447
+ }
448
+ else {
449
+ console.log(chalk.red(' āœ— Not running'));
450
+ }
451
+ // Stats
452
+ if (stats) {
453
+ console.log();
454
+ console.log(chalk.cyan('Stats:'));
455
+ console.log(` Port: ${stats.port}`);
456
+ console.log(` Uptime: ${formatUptime(Date.now() - stats.startTime)}`);
457
+ console.log(` Requests: ${stats.requestCount}`);
458
+ if (stats.lastRequestTime) {
459
+ const ago = formatUptime(Date.now() - stats.lastRequestTime);
460
+ console.log(` Last request: ${ago} ago`);
461
+ }
462
+ }
463
+ // Configuration
464
+ console.log();
465
+ console.log(chalk.cyan('Configuration:'));
466
+ console.log(` Project: ${config.project_id || chalk.red('Not set')}`);
467
+ console.log(` Claude region: ${config.default_region}`);
468
+ console.log(` Gemini region: ${config.google_region}`);
469
+ console.log(` Default model: ${config.default_model}`);
470
+ // Health check
471
+ if (pid && isRunning(pid) && stats) {
472
+ console.log();
473
+ console.log(chalk.cyan('Health Check:'));
474
+ const spinner = ora('Checking...').start();
475
+ try {
476
+ const response = await fetch(`http://localhost:${stats.port}/health`, {
477
+ signal: AbortSignal.timeout(5000)
478
+ });
479
+ if (response.ok) {
480
+ const data = await response.json();
481
+ spinner.succeed(chalk.green(`Healthy - ${data.requestCount || 0} requests, uptime ${formatUptime((data.uptime || 0) * 1000)}`));
482
+ }
483
+ else {
484
+ spinner.fail(chalk.red(`Unhealthy - HTTP ${response.status}`));
485
+ }
486
+ }
487
+ catch (e) {
488
+ spinner.fail(chalk.red(`Failed - ${e.message}`));
489
+ }
490
+ }
491
+ // Files
492
+ console.log();
493
+ console.log(chalk.cyan('Files:'));
494
+ console.log(` Config: ${CONFIG_FILE} ${fs.existsSync(CONFIG_FILE) ? chalk.green('āœ“') : chalk.gray('(not found)')}`);
495
+ console.log(` PID: ${PID_FILE} ${fs.existsSync(PID_FILE) ? chalk.green('āœ“') : chalk.gray('(not found)')}`);
496
+ console.log(` Logs: ${LOG_FILE} ${fs.existsSync(LOG_FILE) ? chalk.green('āœ“') : chalk.gray('(not found)')}`);
497
+ console.log(` Stats: ${STATS_FILE} ${fs.existsSync(STATS_FILE) ? chalk.green('āœ“') : chalk.gray('(not found)')}`);
498
+ console.log();
499
+ }
500
+ async function showLogs(options) {
501
+ const lines = options.lines || 50;
502
+ if (!fs.existsSync(LOG_FILE)) {
503
+ console.log(chalk.yellow('No log file found. Proxy may not have run yet.'));
504
+ console.log(chalk.gray(`Expected: ${LOG_FILE}`));
505
+ return;
506
+ }
507
+ if (options.follow) {
508
+ console.log(chalk.blue.bold(`šŸ“œ Tailing ${LOG_FILE}\n`));
509
+ console.log(chalk.gray('Press Ctrl+C to exit\n'));
510
+ // Use tail -f
511
+ const tail = spawn('tail', ['-f', '-n', lines.toString(), LOG_FILE], {
512
+ stdio: 'inherit'
513
+ });
514
+ // Handle Ctrl+C
515
+ process.on('SIGINT', () => {
516
+ tail.kill();
55
517
  process.exit(0);
518
+ });
519
+ await new Promise((resolve) => {
520
+ tail.on('close', resolve);
521
+ });
56
522
  }
523
+ else {
524
+ console.log(chalk.blue.bold(`šŸ“œ Last ${lines} lines of ${LOG_FILE}\n`));
525
+ try {
526
+ const content = fs.readFileSync(LOG_FILE, 'utf8');
527
+ const allLines = content.trim().split('\n');
528
+ const lastLines = allLines.slice(-lines);
529
+ for (const line of lastLines) {
530
+ // Color code by level
531
+ if (line.includes('[ERROR]')) {
532
+ console.log(chalk.red(line));
533
+ }
534
+ else if (line.includes('[WARN]')) {
535
+ console.log(chalk.yellow(line));
536
+ }
537
+ else {
538
+ console.log(line);
539
+ }
540
+ }
541
+ console.log();
542
+ console.log(chalk.gray(`Tip: vertex-ai-proxy logs -f (follow mode)`));
543
+ }
544
+ catch (e) {
545
+ console.log(chalk.red(`Error reading log: ${e.message}`));
546
+ }
547
+ }
548
+ }
549
+ // ============================================================================
550
+ // Commands
551
+ // ============================================================================
552
+ const program = new Command();
553
+ program
554
+ .name('vertex-ai-proxy')
555
+ .description('Proxy server for Vertex AI models with OpenClaw support')
556
+ .version(VERSION);
557
+ // --- Daemon management commands ---
558
+ program.command('start')
559
+ .description('Start the proxy as a background daemon')
560
+ .option('-p, --port <port>', 'Port', '8001')
561
+ .option('--project <project>', 'GCP Project ID')
562
+ .option('--region <region>', 'Claude region', 'us-east5')
563
+ .option('--google-region <region>', 'Gemini region', 'us-central1')
564
+ .action(startDaemon);
565
+ program.command('stop')
566
+ .description('Stop the background daemon')
567
+ .action(stopDaemon);
568
+ program.command('restart')
569
+ .description('Restart the background daemon')
570
+ .option('-p, --port <port>', 'Port', '8001')
571
+ .option('--project <project>', 'GCP Project ID')
572
+ .option('--region <region>', 'Claude region', 'us-east5')
573
+ .option('--google-region <region>', 'Gemini region', 'us-central1')
574
+ .action(restartDaemon);
575
+ program.command('status')
576
+ .alias('health')
577
+ .description('Show proxy status and health')
578
+ .action(showStatus);
579
+ program.command('logs')
580
+ .description('Show proxy logs')
581
+ .option('-f, --follow', 'Follow log output (tail -f style)')
582
+ .option('-n, --lines <number>', 'Number of lines to show', '50')
583
+ .action(showLogs);
584
+ // --- models command ---
585
+ const modelsCmd = program.command('models').description('List and manage models');
586
+ modelsCmd
587
+ .command('list').alias('ls')
588
+ .description('List all known models')
589
+ .option('-a, --all', 'Show all details')
590
+ .option('-p, --provider <provider>', 'Filter by provider')
591
+ .option('--json', 'Output as JSON')
592
+ .action(listModels);
593
+ modelsCmd.command('fetch')
594
+ .description('Check model availability in Vertex AI')
595
+ .action(fetchModels);
596
+ modelsCmd.command('info <model>')
597
+ .description('Show detailed model info')
598
+ .action(showModelInfo);
599
+ modelsCmd.command('enable <model>')
600
+ .description('Enable a model in config')
601
+ .option('--alias <alias>', 'Set an alias')
602
+ .action(enableModel);
603
+ modelsCmd.command('disable <model>')
604
+ .description('Disable a model')
605
+ .action(disableModel);
606
+ modelsCmd.action(() => listModels({}));
607
+ // --- config command ---
608
+ const configCmd = program.command('config').description('Manage configuration');
609
+ configCmd.command('show')
610
+ .description('Show current config')
611
+ .option('--json', 'Output as JSON')
612
+ .action(showConfig);
613
+ configCmd.command('set')
614
+ .description('Interactive config setup')
615
+ .action(interactiveConfig);
616
+ configCmd.command('set-default <model>')
617
+ .description('Set default model')
618
+ .action(setDefaultModel);
619
+ configCmd.command('add-alias <alias> <model>')
620
+ .description('Add model alias')
621
+ .action(addAlias);
622
+ configCmd.command('remove-alias <alias>')
623
+ .description('Remove alias')
624
+ .action(removeAlias);
625
+ configCmd.command('set-fallback <model> <fallbacks...>')
626
+ .description('Set fallback chain')
627
+ .action(setFallback);
628
+ configCmd.command('export')
629
+ .description('Export for OpenClaw')
630
+ .option('-o, --output <file>', 'Output file')
631
+ .action(exportForOpenClaw);
632
+ configCmd.action(() => showConfig({}));
633
+ // --- other commands ---
634
+ program.command('setup-openclaw')
635
+ .description('Configure OpenClaw integration')
636
+ .option('--project <project>', 'GCP Project ID')
637
+ .option('--port <port>', 'Proxy port', '8001')
638
+ .action(setupOpenClaw);
639
+ program.command('check')
640
+ .description('Check Google Cloud setup')
641
+ .action(checkSetup);
642
+ program.command('install-service')
643
+ .description('Install as systemd service')
644
+ .option('--project <project>', 'GCP Project ID')
645
+ .option('--port <port>', 'Proxy port', '8001')
646
+ .option('--user', 'Install as user service')
647
+ .action(installService);
648
+ // Default: start server (foreground)
649
+ program
650
+ .option('-p, --port <port>', 'Port', '8001')
651
+ .option('--project <project>', 'GCP Project ID')
652
+ .option('--region <region>', 'Claude region', 'us-east5')
653
+ .option('--google-region <region>', 'Gemini region', 'us-central1')
654
+ .action((options, command) => {
655
+ if (command.args.length === 0)
656
+ startServer(options);
657
+ });
658
+ // ============================================================================
659
+ // Command Implementations
660
+ // ============================================================================
661
+ async function listModels(options) {
662
+ console.log(chalk.blue.bold('\nšŸ“‹ Available Vertex AI Models\n'));
663
+ const config = loadConfig();
664
+ let models = Object.values(MODEL_CATALOG);
665
+ if (options.provider) {
666
+ models = models.filter(m => m.provider === options.provider);
667
+ }
668
+ if (options.json) {
669
+ console.log(JSON.stringify(models, null, 2));
670
+ return;
671
+ }
672
+ const byProvider = {};
673
+ for (const model of models) {
674
+ if (!byProvider[model.provider])
675
+ byProvider[model.provider] = [];
676
+ byProvider[model.provider].push(model);
677
+ }
678
+ const providerNames = {
679
+ anthropic: 'šŸ¤– Claude (Anthropic)',
680
+ google: '✨ Gemini (Google)',
681
+ imagen: 'šŸŽØ Imagen (Google)'
682
+ };
683
+ for (const [provider, providerModels] of Object.entries(byProvider)) {
684
+ console.log(chalk.yellow.bold(`\n${providerNames[provider] || provider}\n`));
685
+ for (const model of providerModels) {
686
+ const isEnabled = config.enabled_models.includes(model.id);
687
+ const isDefault = config.default_model === model.id;
688
+ const status = isDefault ? chalk.green('ā˜… DEFAULT') :
689
+ isEnabled ? chalk.blue('āœ“ enabled') : chalk.gray('ā—‹');
690
+ console.log(` ${status} ${chalk.white.bold(model.id)}`);
691
+ console.log(` ${chalk.gray(model.name)} - ${model.description}`);
692
+ if (options.all) {
693
+ console.log(` ${chalk.cyan('Context:')} ${(model.contextWindow / 1000).toFixed(0)}K`);
694
+ console.log(` ${chalk.cyan('Price:')} ${formatPrice(model.inputPrice, model.outputPrice)} /1M tok`);
695
+ console.log(` ${chalk.cyan('Regions:')} ${model.regions.join(', ')}`);
696
+ console.log(` ${chalk.cyan('Caps:')} ${formatCapabilities(model.capabilities)}`);
697
+ }
698
+ console.log();
699
+ }
700
+ }
701
+ if (Object.keys(config.model_aliases).length > 0) {
702
+ console.log(chalk.yellow.bold('\nšŸ·ļø Your Aliases\n'));
703
+ for (const [alias, target] of Object.entries(config.model_aliases)) {
704
+ console.log(` ${chalk.cyan(alias)} → ${target}`);
705
+ }
706
+ }
707
+ console.log(chalk.gray('\nTip: vertex-ai-proxy models info <model>'));
708
+ console.log(chalk.gray(' vertex-ai-proxy models enable <model>'));
709
+ }
710
+ async function fetchModels() {
711
+ console.log(chalk.blue.bold('\nšŸ” Checking Vertex AI Models...\n'));
712
+ const config = loadConfig();
713
+ if (!config.project_id) {
714
+ console.log(chalk.red('No project ID. Run: vertex-ai-proxy config set'));
715
+ return;
716
+ }
717
+ const spinner = ora('Checking models...').start();
718
+ for (const [id, model] of Object.entries(MODEL_CATALOG)) {
719
+ if (model.provider !== 'anthropic')
720
+ continue;
721
+ spinner.text = `Checking ${model.name}...`;
722
+ try {
723
+ execSync(`gcloud ai models describe publishers/anthropic/models/${id.split('@')[0]} --region=${config.default_region} --project=${config.project_id} 2>&1`, { encoding: 'utf8', timeout: 10000 });
724
+ MODEL_CATALOG[id].available = true;
725
+ }
726
+ catch (e) {
727
+ MODEL_CATALOG[id].available = e.message?.includes('not found') ? false : undefined;
728
+ }
729
+ }
730
+ spinner.succeed('Check complete');
731
+ console.log(chalk.yellow.bold('\nšŸ“Š Model Availability\n'));
732
+ for (const [id, model] of Object.entries(MODEL_CATALOG)) {
733
+ if (model.provider !== 'anthropic')
734
+ continue;
735
+ const status = model.available === true ? chalk.green('āœ“ Available') :
736
+ model.available === false ? chalk.red('āœ— Not enabled') :
737
+ chalk.yellow('? Unknown');
738
+ console.log(` ${status} ${model.name} (${id})`);
739
+ }
740
+ console.log(chalk.gray('\nEnable models at: https://console.cloud.google.com/vertex-ai/model-garden'));
57
741
  }
58
- const projectId = config.projectId || process.env.PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT;
59
- if (!projectId) {
60
- console.error('Error: PROJECT_ID required');
61
- process.exit(1);
742
+ async function showModelInfo(modelId) {
743
+ let model = MODEL_CATALOG[modelId];
744
+ if (!model) {
745
+ const matches = Object.entries(MODEL_CATALOG).filter(([id, m]) => id.includes(modelId) || m.name.toLowerCase().includes(modelId.toLowerCase()));
746
+ if (matches.length === 0) {
747
+ console.log(chalk.red(`Not found: ${modelId}`));
748
+ return;
749
+ }
750
+ if (matches.length > 1) {
751
+ console.log(chalk.yellow('Multiple matches:'));
752
+ matches.forEach(([id, m]) => console.log(` - ${id} (${m.name})`));
753
+ return;
754
+ }
755
+ model = matches[0][1];
756
+ }
757
+ const config = loadConfig();
758
+ console.log(chalk.blue.bold(`\nšŸ“– ${model.name}\n`));
759
+ console.log(chalk.gray('─'.repeat(50)));
760
+ console.log(`${chalk.cyan('ID:')} ${model.id}`);
761
+ console.log(`${chalk.cyan('Provider:')} ${model.provider}`);
762
+ console.log(`${chalk.cyan('Description:')} ${model.description}`);
763
+ console.log();
764
+ console.log(`${chalk.cyan('Context:')} ${(model.contextWindow / 1000).toFixed(0)}K tokens`);
765
+ console.log(`${chalk.cyan('Max Output:')} ${model.maxTokens} tokens`);
766
+ console.log(`${chalk.cyan('Price:')} $${model.inputPrice} in / $${model.outputPrice} out (per 1M)`);
767
+ console.log();
768
+ console.log(`${chalk.cyan('Regions:')} ${model.regions.join(', ')}`);
769
+ console.log(`${chalk.cyan('Capabilities:')} ${formatCapabilities(model.capabilities)}`);
770
+ console.log(chalk.gray('─'.repeat(50)));
771
+ const isDefault = config.default_model === model.id;
772
+ const isEnabled = config.enabled_models.includes(model.id);
773
+ if (isDefault)
774
+ console.log(chalk.green('ā˜… This is your default model'));
775
+ else if (isEnabled)
776
+ console.log(chalk.blue('āœ“ Enabled in your config'));
777
+ else
778
+ console.log(chalk.gray(`ā—‹ Not enabled. Run: vertex-ai-proxy models enable ${model.id}`));
779
+ const aliases = Object.entries(config.model_aliases)
780
+ .filter(([_, t]) => t === model.id).map(([a]) => a);
781
+ if (aliases.length > 0)
782
+ console.log(chalk.cyan(`\nAliases: ${aliases.join(', ')}`));
783
+ }
784
+ async function enableModel(modelId, options) {
785
+ const model = MODEL_CATALOG[modelId];
786
+ if (!model) {
787
+ console.log(chalk.red(`Not found: ${modelId}`));
788
+ return;
789
+ }
790
+ const config = loadConfig();
791
+ if (!config.enabled_models.includes(modelId)) {
792
+ config.enabled_models.push(modelId);
793
+ }
794
+ if (options.alias) {
795
+ config.model_aliases[options.alias] = modelId;
796
+ }
797
+ saveConfig(config);
798
+ console.log(chalk.green(`āœ“ Enabled ${model.name}`));
799
+ if (options.alias)
800
+ console.log(chalk.blue(` Alias: ${options.alias} → ${modelId}`));
801
+ }
802
+ async function disableModel(modelId) {
803
+ const config = loadConfig();
804
+ config.enabled_models = config.enabled_models.filter(m => m !== modelId);
805
+ for (const [alias, target] of Object.entries(config.model_aliases)) {
806
+ if (target === modelId)
807
+ delete config.model_aliases[alias];
808
+ }
809
+ saveConfig(config);
810
+ console.log(chalk.yellow(`āœ“ Disabled ${modelId}`));
811
+ }
812
+ async function showConfig(options) {
813
+ const config = loadConfig();
814
+ if (options.json) {
815
+ console.log(JSON.stringify(config, null, 2));
816
+ return;
817
+ }
818
+ console.log(chalk.blue.bold('\nāš™ļø Configuration\n'));
819
+ console.log(`${chalk.cyan('Config file:')} ${CONFIG_FILE}`);
820
+ console.log(`${chalk.cyan('Project ID:')} ${config.project_id || chalk.red('Not set')}`);
821
+ console.log(`${chalk.cyan('Claude region:')} ${config.default_region}`);
822
+ console.log(`${chalk.cyan('Gemini region:')} ${config.google_region}`);
823
+ console.log(`${chalk.cyan('Default model:')} ${config.default_model}`);
824
+ if (config.enabled_models.length > 0) {
825
+ console.log(chalk.yellow.bold('\nšŸ“¦ Enabled Models\n'));
826
+ config.enabled_models.forEach(m => {
827
+ const model = MODEL_CATALOG[m];
828
+ console.log(` • ${m} ${chalk.gray(`(${model?.name || 'unknown'})`)}`);
829
+ });
830
+ }
831
+ if (Object.keys(config.model_aliases).length > 0) {
832
+ console.log(chalk.yellow.bold('\nšŸ·ļø Aliases\n'));
833
+ for (const [alias, target] of Object.entries(config.model_aliases)) {
834
+ console.log(` ${chalk.cyan(alias)} → ${target}`);
835
+ }
836
+ }
837
+ if (Object.keys(config.fallback_chains).length > 0) {
838
+ console.log(chalk.yellow.bold('\nšŸ”€ Fallbacks\n'));
839
+ for (const [model, fallbacks] of Object.entries(config.fallback_chains)) {
840
+ console.log(` ${model}`);
841
+ fallbacks.forEach((f, i) => console.log(` ${i + 1}. ${f}`));
842
+ }
843
+ }
844
+ }
845
+ async function interactiveConfig() {
846
+ console.log(chalk.blue.bold('\nāš™ļø Interactive Configuration\n'));
847
+ const config = loadConfig();
848
+ // Project ID
849
+ let currentProject = config.project_id;
850
+ try {
851
+ currentProject = currentProject || execSync('gcloud config get-value project 2>/dev/null', { encoding: 'utf8' }).trim();
852
+ }
853
+ catch (e) { }
854
+ const projectId = await prompt(chalk.cyan(`Project ID [${currentProject || 'none'}]: `)) || currentProject;
855
+ if (projectId)
856
+ config.project_id = projectId;
857
+ // Default model
858
+ console.log(chalk.yellow('\nšŸ“¦ Select default model:\n'));
859
+ const modelOptions = [
860
+ 'claude-opus-4-5@20251101 - Most capable ($$)',
861
+ 'claude-sonnet-4-5@20250514 - Balanced ($)',
862
+ 'claude-haiku-4-5@20251001 - Fast & cheap',
863
+ 'gemini-2.5-pro - Google\'s best',
864
+ 'gemini-2.5-flash - Fast Gemini'
865
+ ];
866
+ const modelIds = [
867
+ 'claude-opus-4-5@20251101', 'claude-sonnet-4-5@20250514', 'claude-haiku-4-5@20251001',
868
+ 'gemini-2.5-pro', 'gemini-2.5-flash'
869
+ ];
870
+ const modelChoice = await promptSelect('', modelOptions);
871
+ config.default_model = modelIds[modelChoice];
872
+ // Enable models
873
+ if (await promptYesNo(chalk.cyan('\nEnable all Claude models?'))) {
874
+ ['claude-opus-4-5@20251101', 'claude-sonnet-4-5@20250514', 'claude-haiku-4-5@20251001']
875
+ .forEach(m => { if (!config.enabled_models.includes(m))
876
+ config.enabled_models.push(m); });
877
+ }
878
+ if (await promptYesNo(chalk.cyan('Enable Gemini models?'))) {
879
+ ['gemini-2.5-pro', 'gemini-2.5-flash']
880
+ .forEach(m => { if (!config.enabled_models.includes(m))
881
+ config.enabled_models.push(m); });
882
+ }
883
+ // Aliases
884
+ if (await promptYesNo(chalk.cyan('Set up common aliases (opus, sonnet, haiku, gpt-4)?'))) {
885
+ config.model_aliases = {
886
+ ...config.model_aliases,
887
+ opus: 'claude-opus-4-5@20251101',
888
+ sonnet: 'claude-sonnet-4-5@20250514',
889
+ haiku: 'claude-haiku-4-5@20251001',
890
+ gemini: 'gemini-2.5-pro',
891
+ 'gemini-flash': 'gemini-2.5-flash',
892
+ 'gpt-4': 'claude-opus-4-5@20251101',
893
+ 'gpt-4o': 'claude-sonnet-4-5@20250514',
894
+ 'gpt-4o-mini': 'claude-haiku-4-5@20251001'
895
+ };
896
+ }
897
+ // Fallbacks
898
+ if (await promptYesNo(chalk.cyan('Set up fallback chains?'))) {
899
+ config.fallback_chains = {
900
+ 'claude-opus-4-5@20251101': ['claude-sonnet-4-5@20250514', 'gemini-2.5-pro'],
901
+ 'claude-sonnet-4-5@20250514': ['claude-haiku-4-5@20251001', 'gemini-2.5-flash'],
902
+ 'claude-haiku-4-5@20251001': ['gemini-2.5-flash-lite']
903
+ };
904
+ }
905
+ saveConfig(config);
906
+ console.log(chalk.green(`\nāœ“ Saved to ${CONFIG_FILE}`));
907
+ // OpenClaw
908
+ if (fs.existsSync(path.join(os.homedir(), '.openclaw'))) {
909
+ if (await promptYesNo(chalk.cyan('\nConfigure OpenClaw?'))) {
910
+ await setupOpenClaw({ project: config.project_id });
911
+ }
912
+ }
913
+ }
914
+ async function setDefaultModel(modelId) {
915
+ const config = loadConfig();
916
+ const resolved = config.model_aliases[modelId] || modelId;
917
+ const model = MODEL_CATALOG[resolved];
918
+ if (!model) {
919
+ console.log(chalk.red(`Not found: ${modelId}`));
920
+ return;
921
+ }
922
+ config.default_model = resolved;
923
+ if (!config.enabled_models.includes(resolved)) {
924
+ config.enabled_models.push(resolved);
925
+ }
926
+ saveConfig(config);
927
+ console.log(chalk.green(`āœ“ Default: ${model.name} (${resolved})`));
928
+ }
929
+ async function addAlias(alias, modelId) {
930
+ if (!MODEL_CATALOG[modelId]) {
931
+ console.log(chalk.red(`Not found: ${modelId}`));
932
+ return;
933
+ }
934
+ const config = loadConfig();
935
+ config.model_aliases[alias] = modelId;
936
+ saveConfig(config);
937
+ console.log(chalk.green(`āœ“ ${alias} → ${modelId}`));
938
+ }
939
+ async function removeAlias(alias) {
940
+ const config = loadConfig();
941
+ if (!config.model_aliases[alias]) {
942
+ console.log(chalk.yellow(`Not found: ${alias}`));
943
+ return;
944
+ }
945
+ delete config.model_aliases[alias];
946
+ saveConfig(config);
947
+ console.log(chalk.green(`āœ“ Removed ${alias}`));
948
+ }
949
+ async function setFallback(modelId, fallbacks) {
950
+ const config = loadConfig();
951
+ config.fallback_chains[modelId] = fallbacks;
952
+ saveConfig(config);
953
+ console.log(chalk.green(`āœ“ Fallbacks for ${modelId}:`));
954
+ fallbacks.forEach((f, i) => console.log(` ${i + 1}. ${f}`));
955
+ }
956
+ async function exportForOpenClaw(options) {
957
+ const config = loadConfig();
958
+ const openclawConfig = {
959
+ env: {
960
+ GOOGLE_CLOUD_PROJECT: config.project_id,
961
+ GOOGLE_CLOUD_LOCATION: config.default_region
962
+ },
963
+ agents: {
964
+ defaults: {
965
+ model: {
966
+ primary: `vertex/${config.default_model}`,
967
+ fallbacks: config.fallback_chains[config.default_model]?.map(m => `vertex/${m}`) || []
968
+ }
969
+ }
970
+ },
971
+ models: {
972
+ mode: 'merge',
973
+ providers: {
974
+ vertex: {
975
+ baseUrl: 'http://localhost:8001/v1',
976
+ apiKey: 'vertex-proxy',
977
+ api: 'anthropic-messages',
978
+ models: config.enabled_models.map(id => {
979
+ const m = MODEL_CATALOG[id];
980
+ return {
981
+ id,
982
+ name: m?.name || id,
983
+ input: m?.capabilities.includes('vision') ? ['text', 'image'] : ['text'],
984
+ contextWindow: m?.contextWindow || 200000,
985
+ maxTokens: m?.maxTokens || 8192
986
+ };
987
+ })
988
+ }
989
+ }
990
+ }
991
+ };
992
+ const output = JSON.stringify(openclawConfig, null, 2);
993
+ if (options.output) {
994
+ fs.writeFileSync(options.output, output);
995
+ console.log(chalk.green(`āœ“ Exported to ${options.output}`));
996
+ }
997
+ else {
998
+ console.log(chalk.blue.bold('\nšŸ“‹ OpenClaw Config\n'));
999
+ console.log(chalk.gray('Add to ~/.openclaw/openclaw.json:\n'));
1000
+ console.log(output);
1001
+ }
1002
+ }
1003
+ async function setupOpenClaw(options) {
1004
+ console.log(chalk.blue.bold('\nšŸ¦ž OpenClaw Setup\n'));
1005
+ const config = loadConfig();
1006
+ let projectId = options.project || config.project_id;
1007
+ if (!projectId) {
1008
+ projectId = await prompt(chalk.cyan('GCP Project ID: '));
1009
+ if (!projectId) {
1010
+ console.log(chalk.red('Required.'));
1011
+ return;
1012
+ }
1013
+ config.project_id = projectId;
1014
+ saveConfig(config);
1015
+ }
1016
+ await exportForOpenClaw({});
1017
+ console.log(chalk.blue('\nšŸ“‹ Next:\n'));
1018
+ console.log(' 1. Add config to ~/.openclaw/openclaw.json');
1019
+ console.log(' 2. Start proxy: vertex-ai-proxy start');
1020
+ console.log(' 3. Restart OpenClaw: openclaw gateway restart');
1021
+ }
1022
+ async function checkSetup() {
1023
+ console.log(chalk.blue.bold('\nšŸ” Checking Setup\n'));
1024
+ const s1 = ora('gcloud CLI...').start();
1025
+ try {
1026
+ const v = execSync('gcloud --version', { encoding: 'utf8' }).split('\n')[0];
1027
+ s1.succeed(`gcloud: ${v}`);
1028
+ }
1029
+ catch (e) {
1030
+ s1.fail('gcloud not found');
1031
+ console.log(chalk.yellow(' https://cloud.google.com/sdk/docs/install'));
1032
+ return;
1033
+ }
1034
+ const s2 = ora('Authentication...').start();
1035
+ try {
1036
+ const account = execSync('gcloud config get-value account', { encoding: 'utf8' }).trim();
1037
+ if (account)
1038
+ s2.succeed(`Auth: ${account}`);
1039
+ else {
1040
+ s2.fail('Not authenticated');
1041
+ console.log(chalk.yellow(' gcloud auth login'));
1042
+ return;
1043
+ }
1044
+ }
1045
+ catch (e) {
1046
+ s2.fail('Auth check failed');
1047
+ return;
1048
+ }
1049
+ const s3 = ora('ADC...').start();
1050
+ const adcPath = path.join(os.homedir(), '.config', 'gcloud', 'application_default_credentials.json');
1051
+ if (fs.existsSync(adcPath))
1052
+ s3.succeed('ADC configured');
1053
+ else {
1054
+ s3.fail('ADC missing');
1055
+ console.log(chalk.yellow(' gcloud auth application-default login'));
1056
+ return;
1057
+ }
1058
+ const s4 = ora('Project...').start();
1059
+ const config = loadConfig();
1060
+ let project = config.project_id;
1061
+ try {
1062
+ project = project || execSync('gcloud config get-value project', { encoding: 'utf8' }).trim();
1063
+ }
1064
+ catch (e) { }
1065
+ if (project)
1066
+ s4.succeed(`Project: ${project}`);
1067
+ else {
1068
+ s4.warn('No project');
1069
+ console.log(chalk.yellow(' vertex-ai-proxy config set'));
1070
+ }
1071
+ console.log(chalk.green('\nāœ“ Ready!\n'));
1072
+ }
1073
+ async function installService(options) {
1074
+ console.log(chalk.blue.bold('\nšŸ”§ Install Service\n'));
1075
+ const config = loadConfig();
1076
+ const projectId = options.project || config.project_id;
1077
+ const port = options.port || '8001';
1078
+ if (!projectId) {
1079
+ console.log(chalk.red('Project required. Use --project or run config set'));
1080
+ return;
1081
+ }
1082
+ const user = os.userInfo().username;
1083
+ const home = os.homedir();
1084
+ const nodePath = process.execPath;
1085
+ const adcPath = path.join(home, '.config', 'gcloud', 'application_default_credentials.json');
1086
+ const service = `[Unit]
1087
+ Description=Vertex AI Proxy
1088
+ After=network.target
1089
+
1090
+ [Service]
1091
+ Type=simple
1092
+ User=${user}
1093
+ Environment="GOOGLE_CLOUD_PROJECT=${projectId}"
1094
+ Environment="VERTEX_PROXY_PORT=${port}"
1095
+ Environment="GOOGLE_APPLICATION_CREDENTIALS=${adcPath}"
1096
+ ExecStart=${nodePath} ${process.argv[1]} --port ${port}
1097
+ Restart=always
1098
+ RestartSec=10
1099
+
1100
+ [Install]
1101
+ WantedBy=multi-user.target
1102
+ `;
1103
+ if (options.user) {
1104
+ const serviceDir = path.join(home, '.config', 'systemd', 'user');
1105
+ const servicePath = path.join(serviceDir, 'vertex-ai-proxy.service');
1106
+ fs.mkdirSync(serviceDir, { recursive: true });
1107
+ fs.writeFileSync(servicePath, service);
1108
+ console.log(chalk.green(`āœ“ Created ${servicePath}\n`));
1109
+ console.log(chalk.yellow('Run:'));
1110
+ console.log(' systemctl --user daemon-reload');
1111
+ console.log(' systemctl --user enable vertex-ai-proxy');
1112
+ console.log(' systemctl --user start vertex-ai-proxy');
1113
+ }
1114
+ else {
1115
+ const servicePath = '/tmp/vertex-ai-proxy.service';
1116
+ fs.writeFileSync(servicePath, service);
1117
+ console.log(chalk.yellow('Run (sudo required):'));
1118
+ console.log(` sudo cp ${servicePath} /etc/systemd/system/`);
1119
+ console.log(' sudo systemctl daemon-reload');
1120
+ console.log(' sudo systemctl enable vertex-ai-proxy');
1121
+ console.log(' sudo systemctl start vertex-ai-proxy');
1122
+ }
1123
+ }
1124
+ async function startServer(options) {
1125
+ console.log(chalk.blue.bold('\nšŸš€ Vertex AI Proxy\n'));
1126
+ const config = loadConfig();
1127
+ let projectId = options.project || config.project_id || process.env.GOOGLE_CLOUD_PROJECT;
1128
+ if (!projectId) {
1129
+ console.log(chalk.yellow('āš ļø No project ID.\n'));
1130
+ projectId = await prompt(chalk.cyan('GCP Project ID: '));
1131
+ if (!projectId) {
1132
+ console.log(chalk.red('Required.'));
1133
+ process.exit(1);
1134
+ }
1135
+ }
1136
+ process.env.GOOGLE_CLOUD_PROJECT = projectId;
1137
+ process.env.VERTEX_PROXY_PORT = options.port;
1138
+ process.env.VERTEX_PROXY_REGION = options.region || config.default_region;
1139
+ process.env.VERTEX_PROXY_GOOGLE_REGION = options.googleRegion || config.google_region;
1140
+ console.log(chalk.gray(` Project: ${projectId}`));
1141
+ console.log(chalk.gray(` Port: ${options.port}`));
1142
+ console.log(chalk.gray(` Region: ${process.env.VERTEX_PROXY_REGION}`));
1143
+ console.log(chalk.gray(` Default: ${config.default_model}\n`));
1144
+ try {
1145
+ // Try dist first, then src for dev
1146
+ let serverModule;
1147
+ const distPath = path.join(path.dirname(process.argv[1]), '..', 'dist', 'index.js');
1148
+ const srcPath = path.join(path.dirname(process.argv[1]), '..', 'src', 'index.js');
1149
+ try {
1150
+ serverModule = await import(distPath);
1151
+ }
1152
+ catch (e) {
1153
+ serverModule = await import(srcPath);
1154
+ }
1155
+ await serverModule.startProxy();
1156
+ }
1157
+ catch (e) {
1158
+ console.log(chalk.red('Failed to start:'), e.message);
1159
+ console.log(chalk.gray('\nBuild first: npm run build'));
1160
+ process.exit(1);
1161
+ }
62
1162
  }
63
- config.projectId = projectId;
64
- startServer(config);
1163
+ // ============================================================================
1164
+ // Run
1165
+ // ============================================================================
1166
+ program.parse();
65
1167
  //# sourceMappingURL=cli.js.map