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/README.md +526 -104
- package/dist/cli.d.ts +23 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1161 -59
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +29 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1025 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/server.d.ts +0 -22
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -383
- package/dist/server.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,65 +1,1167 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
1163
|
+
// ============================================================================
|
|
1164
|
+
// Run
|
|
1165
|
+
// ============================================================================
|
|
1166
|
+
program.parse();
|
|
65
1167
|
//# sourceMappingURL=cli.js.map
|