agentic-flow 1.1.6 → 1.1.8

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.
@@ -38,9 +38,10 @@ function getModelForProvider(provider) {
38
38
  };
39
39
  case 'anthropic':
40
40
  default:
41
+ // For anthropic provider, require ANTHROPIC_API_KEY
41
42
  const apiKey = process.env.ANTHROPIC_API_KEY;
42
43
  if (!apiKey) {
43
- throw new Error('ANTHROPIC_API_KEY is required but not set');
44
+ throw new Error('ANTHROPIC_API_KEY is required but not set for Anthropic provider');
44
45
  }
45
46
  return {
46
47
  model: process.env.COMPLETION_MODEL || 'claude-sonnet-4-5-20250929',
@@ -62,59 +63,118 @@ export async function claudeAgent(agent, input, onStream, modelOverride) {
62
63
  // Get model configuration for the selected provider
63
64
  const modelConfig = getModelForProvider(provider);
64
65
  const finalModel = modelOverride || modelConfig.model;
65
- // Set environment variables for the SDK to use correct provider
66
- // The SDK reads ANTHROPIC_API_KEY by default, so we set it based on provider
67
- const originalApiKey = process.env.ANTHROPIC_API_KEY;
68
- if (modelConfig.apiKey && modelConfig.apiKey !== 'local') {
69
- process.env.ANTHROPIC_API_KEY = modelConfig.apiKey;
66
+ // Configure environment for Claude Agent SDK with proxy routing
67
+ // The SDK internally uses Anthropic client which reads ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY
68
+ const envOverrides = {};
69
+ if (provider === 'gemini' && process.env.GOOGLE_GEMINI_API_KEY) {
70
+ // For Gemini: Route through translation proxy
71
+ // Proxy runs on port 3001 and translates Anthropic API → Gemini API
72
+ envOverrides.ANTHROPIC_API_KEY = 'proxy-key'; // Proxy handles real auth
73
+ envOverrides.ANTHROPIC_BASE_URL = process.env.GEMINI_PROXY_URL || 'http://localhost:3001';
74
+ logger.info('Using Gemini proxy', {
75
+ proxyUrl: envOverrides.ANTHROPIC_BASE_URL,
76
+ model: finalModel
77
+ });
78
+ }
79
+ else if (provider === 'openrouter' && process.env.OPENROUTER_API_KEY) {
80
+ // For OpenRouter: Route through translation proxy
81
+ // Proxy runs on port 3000 and translates Anthropic API → OpenRouter API
82
+ envOverrides.ANTHROPIC_API_KEY = 'proxy-key'; // Proxy handles real auth
83
+ envOverrides.ANTHROPIC_BASE_URL = process.env.OPENROUTER_PROXY_URL || 'http://localhost:3000';
84
+ logger.info('Using OpenRouter proxy', {
85
+ proxyUrl: envOverrides.ANTHROPIC_BASE_URL,
86
+ model: finalModel
87
+ });
88
+ }
89
+ else if (provider === 'onnx') {
90
+ // For ONNX: Local inference (TODO: implement ONNX proxy)
91
+ envOverrides.ANTHROPIC_API_KEY = 'local';
92
+ if (modelConfig.baseURL) {
93
+ envOverrides.ANTHROPIC_BASE_URL = modelConfig.baseURL;
94
+ }
70
95
  }
96
+ // For Anthropic provider, use existing ANTHROPIC_API_KEY (no proxy needed)
71
97
  logger.info('Multi-provider configuration', {
72
98
  provider,
73
99
  model: finalModel,
74
- usingRouter: provider !== 'anthropic'
100
+ hasApiKey: !!envOverrides.ANTHROPIC_API_KEY || !!process.env.ANTHROPIC_API_KEY,
101
+ hasBaseURL: !!envOverrides.ANTHROPIC_BASE_URL
75
102
  });
76
103
  try {
77
- // Quad MCP server setup: in-SDK + claude-flow + flow-nexus + agentic-payments
104
+ // MCP server setup - enable in-SDK server and optional external servers
105
+ const mcpServers = {};
106
+ // Enable in-SDK MCP server for custom tools
107
+ if (process.env.ENABLE_CLAUDE_FLOW_SDK === 'true') {
108
+ mcpServers['claude-flow-sdk'] = claudeFlowSdkServer;
109
+ }
110
+ // Optional external MCP servers (disabled by default to avoid subprocess failures)
111
+ // Enable by setting ENABLE_CLAUDE_FLOW_MCP=true or ENABLE_FLOW_NEXUS_MCP=true
112
+ if (process.env.ENABLE_CLAUDE_FLOW_MCP === 'true') {
113
+ mcpServers['claude-flow'] = {
114
+ type: 'stdio',
115
+ command: 'npx',
116
+ args: ['claude-flow@alpha', 'mcp', 'start'],
117
+ env: {
118
+ ...process.env,
119
+ MCP_AUTO_START: 'true',
120
+ PROVIDER: provider
121
+ }
122
+ };
123
+ }
124
+ if (process.env.ENABLE_FLOW_NEXUS_MCP === 'true') {
125
+ mcpServers['flow-nexus'] = {
126
+ type: 'stdio',
127
+ command: 'npx',
128
+ args: ['flow-nexus@latest', 'mcp', 'start'],
129
+ env: {
130
+ ...process.env,
131
+ FLOW_NEXUS_AUTO_START: 'true'
132
+ }
133
+ };
134
+ }
135
+ if (process.env.ENABLE_AGENTIC_PAYMENTS_MCP === 'true') {
136
+ mcpServers['agentic-payments'] = {
137
+ type: 'stdio',
138
+ command: 'npx',
139
+ args: ['-y', 'agentic-payments', 'mcp'],
140
+ env: {
141
+ ...process.env,
142
+ AGENTIC_PAYMENTS_AUTO_START: 'true'
143
+ }
144
+ };
145
+ }
146
+ const queryOptions = {
147
+ systemPrompt: agent.systemPrompt,
148
+ model: finalModel, // Claude Agent SDK handles model selection
149
+ permissionMode: 'bypassPermissions', // Auto-approve all tool usage for Docker automation
150
+ // Enable all built-in tools by default (Read, Write, Edit, Bash, Glob, Grep, WebFetch, WebSearch)
151
+ // Based on SDK types, allowedTools and disallowedTools control which tools are available
152
+ // If not specified, all tools are enabled by default
153
+ allowedTools: [
154
+ 'Read',
155
+ 'Write',
156
+ 'Edit',
157
+ 'Bash',
158
+ 'Glob',
159
+ 'Grep',
160
+ 'WebFetch',
161
+ 'WebSearch',
162
+ 'NotebookEdit',
163
+ 'TodoWrite'
164
+ ],
165
+ // Add MCP servers if configured
166
+ mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined
167
+ };
168
+ // Add environment overrides if present
169
+ if (Object.keys(envOverrides).length > 0) {
170
+ queryOptions.env = {
171
+ ...process.env,
172
+ ...envOverrides
173
+ };
174
+ }
78
175
  const result = query({
79
176
  prompt: input,
80
- options: {
81
- systemPrompt: agent.systemPrompt,
82
- model: finalModel, // Claude Agent SDK handles model selection
83
- permissionMode: 'bypassPermissions', // Auto-approve all tool usage for Docker automation
84
- mcpServers: {
85
- // In-SDK server: 6 basic tools (memory + swarm)
86
- 'claude-flow-sdk': claudeFlowSdkServer,
87
- // Full MCP server: 101 tools via subprocess (neural, analysis, workflow, github, daa, system)
88
- 'claude-flow': {
89
- command: 'npx',
90
- args: ['claude-flow@alpha', 'mcp', 'start'],
91
- env: {
92
- ...process.env,
93
- MCP_AUTO_START: 'true',
94
- PROVIDER: provider
95
- }
96
- },
97
- // Flow Nexus MCP server: 96 cloud tools (sandboxes, swarms, neural, workflows)
98
- 'flow-nexus': {
99
- command: 'npx',
100
- args: ['flow-nexus@latest', 'mcp', 'start'],
101
- env: {
102
- ...process.env,
103
- FLOW_NEXUS_AUTO_START: 'true'
104
- }
105
- },
106
- // Agentic Payments MCP server: Payment authorization and multi-agent consensus
107
- 'agentic-payments': {
108
- command: 'npx',
109
- args: ['-y', 'agentic-payments', 'mcp'],
110
- env: {
111
- ...process.env,
112
- AGENTIC_PAYMENTS_AUTO_START: 'true'
113
- }
114
- }
115
- }
116
- // allowedTools: removed to enable ALL tools from all servers
117
- }
177
+ options: queryOptions
118
178
  });
119
179
  let output = '';
120
180
  for await (const msg of result) {
@@ -135,11 +195,13 @@ export async function claudeAgent(agent, input, onStream, modelOverride) {
135
195
  });
136
196
  return { output, agent: agent.name };
137
197
  }
138
- finally {
139
- // Restore original API key
140
- if (originalApiKey) {
141
- process.env.ANTHROPIC_API_KEY = originalApiKey;
142
- }
198
+ catch (error) {
199
+ logger.error('Claude Agent SDK execution failed', {
200
+ provider,
201
+ model: finalModel,
202
+ error: error.message
203
+ });
204
+ throw error;
143
205
  }
144
206
  });
145
207
  }
@@ -1,12 +1,31 @@
1
+ // Direct API agent with multi-provider support (Anthropic, OpenRouter, Gemini)
2
+ import Anthropic from '@anthropic-ai/sdk';
1
3
  import { logger } from '../utils/logger.js';
2
4
  import { withRetry } from '../utils/retry.js';
3
5
  import { execSync } from 'child_process';
4
6
  import { ModelRouter } from '../router/router.js';
5
- // Lazy initialize router
7
+ // Lazy initialize clients
8
+ let anthropic = null;
6
9
  let router = null;
10
+ function getAnthropicClient() {
11
+ if (!anthropic) {
12
+ const apiKey = process.env.ANTHROPIC_API_KEY;
13
+ // Validate API key format
14
+ if (!apiKey) {
15
+ throw new Error('ANTHROPIC_API_KEY is required but not set');
16
+ }
17
+ if (!apiKey.startsWith('sk-ant-')) {
18
+ throw new Error(`Invalid ANTHROPIC_API_KEY format. Expected format: sk-ant-...\n` +
19
+ `Got: ${apiKey.substring(0, 10)}...\n\n` +
20
+ `Please check your API key at: https://console.anthropic.com/settings/keys`);
21
+ }
22
+ anthropic = new Anthropic({ apiKey });
23
+ }
24
+ return anthropic;
25
+ }
7
26
  function getRouter() {
8
27
  if (!router) {
9
- // Router handles multi-provider logic and creates config from environment variables
28
+ // Router will now auto-create config from environment variables if no file exists
10
29
  router = new ModelRouter();
11
30
  }
12
31
  return router;
@@ -234,8 +253,7 @@ export async function directApiAgent(agent, input, onStream) {
234
253
  : (process.env.COMPLETION_MODEL || 'meta-llama/llama-3.1-8b-instruct'),
235
254
  messages: messagesWithSystem,
236
255
  maxTokens: 8192,
237
- temperature: 0.7,
238
- provider: provider // Force the router to use this specific provider
256
+ temperature: 0.7
239
257
  };
240
258
  const routerResponse = await routerInstance.chat(params);
241
259
  // Convert router response to Anthropic format
package/dist/cli-proxy.js CHANGED
File without changes
@@ -0,0 +1,345 @@
1
+ // Anthropic to Gemini Proxy Server
2
+ // Converts Anthropic API format to Google Gemini format
3
+ import express from 'express';
4
+ import { logger } from '../utils/logger.js';
5
+ export class AnthropicToGeminiProxy {
6
+ app;
7
+ geminiApiKey;
8
+ geminiBaseUrl;
9
+ defaultModel;
10
+ constructor(config) {
11
+ this.app = express();
12
+ this.geminiApiKey = config.geminiApiKey;
13
+ this.geminiBaseUrl = config.geminiBaseUrl || 'https://generativelanguage.googleapis.com/v1beta';
14
+ this.defaultModel = config.defaultModel || 'gemini-2.0-flash-exp';
15
+ this.setupMiddleware();
16
+ this.setupRoutes();
17
+ }
18
+ setupMiddleware() {
19
+ // Parse JSON bodies
20
+ this.app.use(express.json({ limit: '50mb' }));
21
+ // Logging middleware
22
+ this.app.use((req, res, next) => {
23
+ logger.debug('Gemini proxy request', {
24
+ method: req.method,
25
+ path: req.path,
26
+ headers: Object.keys(req.headers)
27
+ });
28
+ next();
29
+ });
30
+ }
31
+ setupRoutes() {
32
+ // Health check
33
+ this.app.get('/health', (req, res) => {
34
+ res.json({ status: 'ok', service: 'anthropic-to-gemini-proxy' });
35
+ });
36
+ // Anthropic Messages API → Gemini generateContent
37
+ this.app.post('/v1/messages', async (req, res) => {
38
+ try {
39
+ const anthropicReq = req.body;
40
+ // Convert Anthropic format to Gemini format
41
+ const geminiReq = this.convertAnthropicToGemini(anthropicReq);
42
+ logger.info('Converting Anthropic request to Gemini', {
43
+ anthropicModel: anthropicReq.model,
44
+ geminiModel: this.defaultModel,
45
+ messageCount: geminiReq.contents.length,
46
+ stream: anthropicReq.stream,
47
+ apiKeyPresent: !!this.geminiApiKey,
48
+ apiKeyPrefix: this.geminiApiKey?.substring(0, 10)
49
+ });
50
+ // Determine endpoint based on streaming
51
+ const endpoint = anthropicReq.stream ? 'streamGenerateContent' : 'generateContent';
52
+ const url = `${this.geminiBaseUrl}/models/${this.defaultModel}:${endpoint}?key=${this.geminiApiKey}`;
53
+ // Forward to Gemini
54
+ const response = await fetch(url, {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json'
58
+ },
59
+ body: JSON.stringify(geminiReq)
60
+ });
61
+ if (!response.ok) {
62
+ const error = await response.text();
63
+ logger.error('Gemini API error', { status: response.status, error });
64
+ return res.status(response.status).json({
65
+ error: {
66
+ type: 'api_error',
67
+ message: error
68
+ }
69
+ });
70
+ }
71
+ // Handle streaming vs non-streaming
72
+ if (anthropicReq.stream) {
73
+ // Stream response
74
+ res.setHeader('Content-Type', 'text/event-stream');
75
+ res.setHeader('Cache-Control', 'no-cache');
76
+ res.setHeader('Connection', 'keep-alive');
77
+ const reader = response.body?.getReader();
78
+ if (!reader) {
79
+ throw new Error('No response body');
80
+ }
81
+ const decoder = new TextDecoder();
82
+ while (true) {
83
+ const { done, value } = await reader.read();
84
+ if (done)
85
+ break;
86
+ const chunk = decoder.decode(value);
87
+ const anthropicChunk = this.convertGeminiStreamToAnthropic(chunk);
88
+ res.write(anthropicChunk);
89
+ }
90
+ res.end();
91
+ }
92
+ else {
93
+ // Non-streaming response
94
+ const geminiRes = await response.json();
95
+ const anthropicRes = this.convertGeminiToAnthropic(geminiRes);
96
+ logger.info('Gemini proxy response sent', {
97
+ model: this.defaultModel,
98
+ usage: anthropicRes.usage
99
+ });
100
+ res.json(anthropicRes);
101
+ }
102
+ }
103
+ catch (error) {
104
+ logger.error('Gemini proxy error', { error: error.message, stack: error.stack });
105
+ res.status(500).json({
106
+ error: {
107
+ type: 'proxy_error',
108
+ message: error.message
109
+ }
110
+ });
111
+ }
112
+ });
113
+ // Fallback for other Anthropic API endpoints
114
+ this.app.use((req, res) => {
115
+ logger.warn('Unsupported endpoint', { path: req.path, method: req.method });
116
+ res.status(404).json({
117
+ error: {
118
+ type: 'not_found',
119
+ message: `Endpoint ${req.path} not supported by Gemini proxy`
120
+ }
121
+ });
122
+ });
123
+ }
124
+ convertAnthropicToGemini(anthropicReq) {
125
+ const contents = [];
126
+ // Add system message as first user message if present
127
+ // Gemini doesn't have a dedicated system role, so we prepend it to the first user message
128
+ let systemPrefix = '';
129
+ if (anthropicReq.system) {
130
+ systemPrefix = `System: ${anthropicReq.system}\n\n`;
131
+ }
132
+ // Add tool instructions for Gemini to understand file operations
133
+ // Since Gemini doesn't have native tool calling, we instruct it to use structured XML-like commands
134
+ const toolInstructions = `
135
+ IMPORTANT: You have access to file system operations through structured commands. Use these exact formats:
136
+
137
+ <file_write path="filename.ext">
138
+ content here
139
+ </file_write>
140
+
141
+ <file_read path="filename.ext"/>
142
+
143
+ <bash_command>
144
+ command here
145
+ </bash_command>
146
+
147
+ When you need to create, edit, or read files, use these structured commands in your response.
148
+ The system will automatically execute these commands and provide results.
149
+
150
+ `;
151
+ // Prepend tool instructions to system prompt
152
+ if (systemPrefix) {
153
+ systemPrefix = toolInstructions + systemPrefix;
154
+ }
155
+ else {
156
+ systemPrefix = toolInstructions;
157
+ }
158
+ // Convert Anthropic messages to Gemini format
159
+ for (let i = 0; i < anthropicReq.messages.length; i++) {
160
+ const msg = anthropicReq.messages[i];
161
+ let text;
162
+ if (typeof msg.content === 'string') {
163
+ text = msg.content;
164
+ }
165
+ else if (Array.isArray(msg.content)) {
166
+ // Extract text from content blocks
167
+ text = msg.content
168
+ .filter(block => block.type === 'text')
169
+ .map(block => block.text)
170
+ .join('\n');
171
+ }
172
+ else {
173
+ text = '';
174
+ }
175
+ // Add system prefix to first user message
176
+ if (i === 0 && msg.role === 'user' && systemPrefix) {
177
+ text = systemPrefix + text;
178
+ }
179
+ contents.push({
180
+ role: msg.role === 'assistant' ? 'model' : 'user',
181
+ parts: [{ text }]
182
+ });
183
+ }
184
+ const geminiReq = {
185
+ contents
186
+ };
187
+ // Add generation config if temperature or max_tokens specified
188
+ if (anthropicReq.temperature !== undefined || anthropicReq.max_tokens !== undefined) {
189
+ geminiReq.generationConfig = {};
190
+ if (anthropicReq.temperature !== undefined) {
191
+ geminiReq.generationConfig.temperature = anthropicReq.temperature;
192
+ }
193
+ if (anthropicReq.max_tokens !== undefined) {
194
+ geminiReq.generationConfig.maxOutputTokens = anthropicReq.max_tokens;
195
+ }
196
+ }
197
+ return geminiReq;
198
+ }
199
+ parseStructuredCommands(text) {
200
+ const toolUses = [];
201
+ let cleanText = text;
202
+ // Parse file_write commands
203
+ const fileWriteRegex = /<file_write path="([^"]+)">([\s\S]*?)<\/file_write>/g;
204
+ let match;
205
+ while ((match = fileWriteRegex.exec(text)) !== null) {
206
+ toolUses.push({
207
+ type: 'tool_use',
208
+ id: `tool_${Date.now()}_${toolUses.length}`,
209
+ name: 'Write',
210
+ input: {
211
+ file_path: match[1],
212
+ content: match[2].trim()
213
+ }
214
+ });
215
+ cleanText = cleanText.replace(match[0], `[File written: ${match[1]}]`);
216
+ }
217
+ // Parse file_read commands
218
+ const fileReadRegex = /<file_read path="([^"]+)"\/>/g;
219
+ while ((match = fileReadRegex.exec(text)) !== null) {
220
+ toolUses.push({
221
+ type: 'tool_use',
222
+ id: `tool_${Date.now()}_${toolUses.length}`,
223
+ name: 'Read',
224
+ input: {
225
+ file_path: match[1]
226
+ }
227
+ });
228
+ cleanText = cleanText.replace(match[0], `[Reading file: ${match[1]}]`);
229
+ }
230
+ // Parse bash commands
231
+ const bashRegex = /<bash_command>([\s\S]*?)<\/bash_command>/g;
232
+ while ((match = bashRegex.exec(text)) !== null) {
233
+ toolUses.push({
234
+ type: 'tool_use',
235
+ id: `tool_${Date.now()}_${toolUses.length}`,
236
+ name: 'Bash',
237
+ input: {
238
+ command: match[1].trim()
239
+ }
240
+ });
241
+ cleanText = cleanText.replace(match[0], `[Executing: ${match[1].trim()}]`);
242
+ }
243
+ return { cleanText: cleanText.trim(), toolUses };
244
+ }
245
+ convertGeminiToAnthropic(geminiRes) {
246
+ const candidate = geminiRes.candidates?.[0];
247
+ if (!candidate) {
248
+ throw new Error('No candidates in Gemini response');
249
+ }
250
+ const content = candidate.content;
251
+ const rawText = content?.parts?.map((part) => part.text).join('') || '';
252
+ // Parse structured commands from Gemini's response
253
+ const { cleanText, toolUses } = this.parseStructuredCommands(rawText);
254
+ // Build content array with text and tool uses
255
+ const contentBlocks = [];
256
+ if (cleanText) {
257
+ contentBlocks.push({
258
+ type: 'text',
259
+ text: cleanText
260
+ });
261
+ }
262
+ // Add tool uses
263
+ contentBlocks.push(...toolUses);
264
+ return {
265
+ id: `msg_${Date.now()}`,
266
+ type: 'message',
267
+ role: 'assistant',
268
+ model: this.defaultModel,
269
+ content: contentBlocks.length > 0 ? contentBlocks : [
270
+ {
271
+ type: 'text',
272
+ text: rawText
273
+ }
274
+ ],
275
+ stop_reason: this.mapFinishReason(candidate.finishReason),
276
+ usage: {
277
+ input_tokens: geminiRes.usageMetadata?.promptTokenCount || 0,
278
+ output_tokens: geminiRes.usageMetadata?.candidatesTokenCount || 0
279
+ }
280
+ };
281
+ }
282
+ convertGeminiStreamToAnthropic(chunk) {
283
+ // Gemini streaming returns newline-delimited JSON
284
+ const lines = chunk.split('\n').filter(line => line.trim());
285
+ const anthropicChunks = [];
286
+ for (const line of lines) {
287
+ try {
288
+ const parsed = JSON.parse(line);
289
+ const candidate = parsed.candidates?.[0];
290
+ const text = candidate?.content?.parts?.[0]?.text;
291
+ if (text) {
292
+ anthropicChunks.push(`event: content_block_delta\ndata: ${JSON.stringify({
293
+ type: 'content_block_delta',
294
+ delta: { type: 'text_delta', text }
295
+ })}\n\n`);
296
+ }
297
+ // Check for finish
298
+ if (candidate?.finishReason) {
299
+ anthropicChunks.push('event: message_stop\ndata: {}\n\n');
300
+ }
301
+ }
302
+ catch (e) {
303
+ // Ignore parse errors
304
+ }
305
+ }
306
+ return anthropicChunks.join('');
307
+ }
308
+ mapFinishReason(reason) {
309
+ const mapping = {
310
+ 'STOP': 'end_turn',
311
+ 'MAX_TOKENS': 'max_tokens',
312
+ 'SAFETY': 'stop_sequence',
313
+ 'RECITATION': 'stop_sequence',
314
+ 'OTHER': 'end_turn'
315
+ };
316
+ return mapping[reason || 'STOP'] || 'end_turn';
317
+ }
318
+ start(port) {
319
+ this.app.listen(port, () => {
320
+ logger.info('Anthropic to Gemini proxy started', {
321
+ port,
322
+ geminiBaseUrl: this.geminiBaseUrl,
323
+ defaultModel: this.defaultModel
324
+ });
325
+ console.log(`\n✅ Gemini Proxy running at http://localhost:${port}`);
326
+ console.log(` Gemini Base URL: ${this.geminiBaseUrl}`);
327
+ console.log(` Default Model: ${this.defaultModel}\n`);
328
+ });
329
+ }
330
+ }
331
+ // CLI entry point
332
+ if (import.meta.url === `file://${process.argv[1]}`) {
333
+ const port = parseInt(process.env.PORT || '3001');
334
+ const geminiApiKey = process.env.GOOGLE_GEMINI_API_KEY;
335
+ if (!geminiApiKey) {
336
+ console.error('❌ Error: GOOGLE_GEMINI_API_KEY environment variable required');
337
+ process.exit(1);
338
+ }
339
+ const proxy = new AnthropicToGeminiProxy({
340
+ geminiApiKey,
341
+ geminiBaseUrl: process.env.GEMINI_BASE_URL,
342
+ defaultModel: process.env.COMPLETION_MODEL || process.env.REASONING_MODEL
343
+ });
344
+ proxy.start(port);
345
+ }