agentic-flow 1.9.2 → 1.9.3

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/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.3] - 2025-11-06
9
+
10
+ ### Fixed - Gemini Provider Now Fully Functional šŸŽ‰
11
+
12
+ **Three Critical Bugs Resolved:**
13
+
14
+ 1. **Model Selection Bug** (cli-proxy.ts:427-431, anthropic-to-gemini.ts)
15
+ - **Issue**: Proxy incorrectly used `COMPLETION_MODEL` environment variable containing `claude-sonnet-4-5-20250929` instead of Gemini model
16
+ - **Fix**: Ignore `COMPLETION_MODEL` for Gemini proxy, always default to `gemini-2.0-flash-exp`
17
+ - **Impact**: Gemini API now receives correct model name
18
+
19
+ 2. **Streaming Response Bug** (anthropic-to-gemini.ts:119-121)
20
+ - **Issue**: Missing `&alt=sse` parameter in streaming API URL caused empty response streams
21
+ - **Fix**: Added `&alt=sse` parameter to `streamGenerateContent` endpoint
22
+ - **Impact**: Streaming responses now work perfectly, returning complete LLM output
23
+
24
+ 3. **Provider Selection Logic Bug** (cli-proxy.ts:299-302)
25
+ - **Issue**: System auto-selected Gemini even when user explicitly specified `--provider anthropic`
26
+ - **Fix**: Check `options.provider` first and return false if user specified different provider
27
+ - **Impact**: Provider flag now correctly overrides auto-detection
28
+
29
+ ### Verified Working
30
+ - āœ… Gemini provider with streaming responses
31
+ - āœ… Anthropic provider (default and explicit)
32
+ - āœ… OpenRouter provider
33
+ - āœ… Non-streaming responses
34
+ - āœ… All three providers tested end-to-end with agents
35
+
36
+ ### Technical Details
37
+ - Direct Gemini API validation confirmed key is valid
38
+ - Proxy correctly converts Anthropic Messages API format to Gemini format
39
+ - Server-Sent Events (SSE) streaming properly parsed and converted
40
+ - All fixes applied to both source (`src/`) and compiled (`dist/`) files
41
+
8
42
  ## [1.8.15] - 2025-11-01
9
43
 
10
44
  ### šŸ› Bug Fix - Model Configuration
package/README.md CHANGED
@@ -246,6 +246,110 @@ npx agentic-flow --agent coder --task "Code cleanup" --optimize --max-cost 0.001
246
246
 
247
247
  ---
248
248
 
249
+ ## šŸ”Œ Provider Support
250
+
251
+ **agentic-flow supports multiple LLM providers** through intelligent proxy architecture that converts requests to provider-specific formats while maintaining Claude Agent SDK compatibility.
252
+
253
+ ### Supported Providers
254
+
255
+ | Provider | Models | Cost | Speed | Setup |
256
+ |----------|--------|------|-------|-------|
257
+ | **Anthropic** | Claude 3.5 Sonnet, Opus, Haiku | $$$ | Fast | `ANTHROPIC_API_KEY` |
258
+ | **Gemini** | Gemini 2.0 Flash, Pro | $ | Very Fast | `GOOGLE_GEMINI_API_KEY` |
259
+ | **OpenRouter** | 100+ models (GPT, Llama, DeepSeek) | Varies | Varies | `OPENROUTER_API_KEY` |
260
+ | **ONNX** | Phi-4 (local) | FREE | Medium | No key needed |
261
+
262
+ ### Quick Provider Examples
263
+
264
+ ```bash
265
+ # Anthropic (default) - Highest quality
266
+ npx agentic-flow --agent coder --task "Build API"
267
+
268
+ # Gemini - Fastest, cost-effective (v1.9.3+)
269
+ export GOOGLE_GEMINI_API_KEY=AIza...
270
+ npx agentic-flow --agent coder --task "Build API" --provider gemini
271
+
272
+ # OpenRouter - 99% cost savings with DeepSeek
273
+ export OPENROUTER_API_KEY=sk-or-...
274
+ npx agentic-flow --agent coder --task "Build API" \
275
+ --provider openrouter \
276
+ --model "deepseek/deepseek-chat"
277
+
278
+ # ONNX - Free local inference (privacy-first)
279
+ npx agentic-flow --agent coder --task "Build API" --provider onnx
280
+ ```
281
+
282
+ ### Provider Architecture
283
+
284
+ **How it works:**
285
+ 1. All requests use Claude Agent SDK format (Messages API)
286
+ 2. Built-in proxies convert to provider-specific formats:
287
+ - **Gemini Proxy**: Converts to `generateContent` API with SSE streaming
288
+ - **OpenRouter Proxy**: Forwards to OpenRouter with model routing
289
+ - **ONNX Proxy**: Routes to local ONNX Runtime with Phi-4
290
+ 3. Responses converted back to Anthropic format
291
+ 4. Full streaming support across all providers
292
+
293
+ **Key Features:**
294
+ - āœ… Streaming responses (real-time output)
295
+ - āœ… Tool calling support (where available)
296
+ - āœ… Automatic format conversion
297
+ - āœ… Error handling and retries
298
+ - āœ… Cost tracking and usage metrics
299
+
300
+ ### Provider Configuration
301
+
302
+ **Environment Variables:**
303
+ ```bash
304
+ # Required for each provider
305
+ ANTHROPIC_API_KEY=sk-ant-... # Anthropic Claude
306
+ GOOGLE_GEMINI_API_KEY=AIza... # Google Gemini
307
+ OPENROUTER_API_KEY=sk-or-v1-... # OpenRouter
308
+ # ONNX requires no key (local inference)
309
+
310
+ # Optional overrides
311
+ PROVIDER=gemini # Force specific provider
312
+ USE_GEMINI=true # Enable Gemini by default
313
+ DEFAULT_MODEL=gemini-2.0-flash-exp # Override model
314
+ ```
315
+
316
+ **CLI Flags:**
317
+ ```bash
318
+ --provider <name> # anthropic, gemini, openrouter, onnx
319
+ --model <name> # Provider-specific model name
320
+ --stream # Enable streaming (default: true)
321
+ --optimize # Auto-select optimal model
322
+ --priority <type> # quality, cost, speed, privacy
323
+ ```
324
+
325
+ ### Gemini Provider (v1.9.3+)
326
+
327
+ **Fully functional** with streaming support! Three critical bugs fixed:
328
+
329
+ ```bash
330
+ # Setup Gemini
331
+ export GOOGLE_GEMINI_API_KEY=AIzaSy...
332
+
333
+ # Use Gemini (fastest responses)
334
+ npx agentic-flow --agent coder --task "Write function" --provider gemini
335
+
336
+ # Gemini with streaming
337
+ npx agentic-flow --agent coder --task "Build API" --provider gemini --stream
338
+
339
+ # Gemini-specific model
340
+ npx agentic-flow --agent coder --task "Task" \
341
+ --provider gemini \
342
+ --model "gemini-2.0-flash-exp"
343
+ ```
344
+
345
+ **Gemini Benefits:**
346
+ - ⚔ **2-5x faster** than Anthropic
347
+ - šŸ’° **70% cheaper** than Claude
348
+ - šŸŽÆ **Excellent for** code generation, analysis, simple tasks
349
+ - āœ… **Full streaming support** (SSE)
350
+
351
+ ---
352
+
249
353
  ## šŸ“‹ CLI Commands
250
354
 
251
355
  ```bash
package/dist/cli-proxy.js CHANGED
@@ -207,7 +207,10 @@ class AgenticFlowCLI {
207
207
  }
208
208
  else if (useGemini) {
209
209
  console.log('šŸš€ Initializing Gemini proxy...');
210
- await this.startGeminiProxy(options.model);
210
+ // Don't pass Anthropic model names to Gemini proxy
211
+ const geminiModel = options.model?.startsWith('claude') ? undefined : options.model;
212
+ console.log(`šŸ” Model filtering: options.model=${options.model}, geminiModel=${geminiModel}`);
213
+ await this.startGeminiProxy(geminiModel);
211
214
  }
212
215
  else {
213
216
  console.log('šŸš€ Using direct Anthropic API...\n');
@@ -248,6 +251,10 @@ class AgenticFlowCLI {
248
251
  if (process.env.USE_GEMINI === 'true') {
249
252
  return true;
250
253
  }
254
+ // BUG FIX: Don't auto-select Gemini if user explicitly specified a different provider
255
+ if (options.provider && options.provider !== 'gemini') {
256
+ return false;
257
+ }
251
258
  if (process.env.GOOGLE_GEMINI_API_KEY &&
252
259
  !process.env.ANTHROPIC_API_KEY &&
253
260
  !process.env.OPENROUTER_API_KEY &&
@@ -347,9 +354,12 @@ class AgenticFlowCLI {
347
354
  process.exit(1);
348
355
  }
349
356
  logger.info('Starting integrated Gemini proxy');
350
- const defaultModel = modelOverride ||
351
- process.env.COMPLETION_MODEL ||
352
- 'gemini-2.0-flash-exp';
357
+ // BUG FIX: Don't use COMPLETION_MODEL for Gemini (it contains Anthropic model names)
358
+ // Always use modelOverride if provided, otherwise default to gemini-2.0-flash-exp
359
+ console.log(`šŸ” Gemini proxy debug: modelOverride=${modelOverride}, COMPLETION_MODEL=${process.env.COMPLETION_MODEL}`);
360
+ const defaultModel = (modelOverride && !modelOverride.startsWith('claude'))
361
+ ? modelOverride
362
+ : 'gemini-2.0-flash-exp';
353
363
  // Import Gemini proxy
354
364
  const { AnthropicToGeminiProxy } = await import('./proxy/anthropic-to-gemini.js');
355
365
  const proxy = new AnthropicToGeminiProxy({
@@ -853,7 +863,11 @@ PERFORMANCE:
853
863
  const streamHandler = options.stream ? (chunk) => process.stdout.write(chunk) : undefined;
854
864
  // FIXED: Use claudeAgentDirect (no Claude Code dependency) instead of claudeAgent
855
865
  // This allows agentic-flow to work standalone in Docker/CI/CD without Claude Code
856
- const result = await claudeAgentDirect(agent, task, streamHandler, options.model);
866
+ // BUG FIX: Don't pass Anthropic model names to non-Anthropic providers
867
+ const modelForAgent = useGemini || useOpenRouter || useONNX || useRequesty
868
+ ? (options.model?.startsWith('claude') ? undefined : options.model)
869
+ : options.model;
870
+ const result = await claudeAgentDirect(agent, task, streamHandler, modelForAgent);
857
871
  if (!options.stream) {
858
872
  console.log('\nāœ… Completed!\n');
859
873
  console.log('═══════════════════════════════════════\n');
@@ -49,7 +49,9 @@ export class AnthropicToGeminiProxy {
49
49
  });
50
50
  // Determine endpoint based on streaming
51
51
  const endpoint = anthropicReq.stream ? 'streamGenerateContent' : 'generateContent';
52
- const url = `${this.geminiBaseUrl}/models/${this.defaultModel}:${endpoint}?key=${this.geminiApiKey}`;
52
+ // BUG FIX: Add &alt=sse for streaming to get Server-Sent Events format
53
+ const streamParam = anthropicReq.stream ? '&alt=sse' : '';
54
+ const url = `${this.geminiBaseUrl}/models/${this.defaultModel}:${endpoint}?key=${this.geminiApiKey}${streamParam}`;
53
55
  // Forward to Gemini
54
56
  const response = await fetch(url, {
55
57
  method: 'POST',
@@ -79,23 +81,39 @@ export class AnthropicToGeminiProxy {
79
81
  throw new Error('No response body');
80
82
  }
81
83
  const decoder = new TextDecoder();
84
+ let chunkCount = 0;
82
85
  while (true) {
83
86
  const { done, value } = await reader.read();
84
87
  if (done)
85
88
  break;
86
89
  const chunk = decoder.decode(value);
90
+ chunkCount++;
91
+ logger.info('Gemini stream chunk received', { chunkCount, chunkLength: chunk.length, chunkPreview: chunk.substring(0, 200) });
87
92
  const anthropicChunk = this.convertGeminiStreamToAnthropic(chunk);
93
+ logger.info('Anthropic stream chunk generated', { chunkCount, anthropicLength: anthropicChunk.length, anthropicPreview: anthropicChunk.substring(0, 200) });
88
94
  res.write(anthropicChunk);
89
95
  }
96
+ logger.info('Gemini stream complete', { totalChunks: chunkCount });
90
97
  res.end();
91
98
  }
92
99
  else {
93
100
  // Non-streaming response
94
101
  const geminiRes = await response.json();
102
+ // DEBUG: Log raw Gemini response
103
+ logger.info('Raw Gemini API response', {
104
+ hasResponse: !!geminiRes,
105
+ hasCandidates: !!geminiRes.candidates,
106
+ candidatesLength: geminiRes.candidates?.length,
107
+ firstCandidate: geminiRes.candidates?.[0],
108
+ fullResponse: JSON.stringify(geminiRes).substring(0, 500)
109
+ });
95
110
  const anthropicRes = this.convertGeminiToAnthropic(geminiRes);
96
111
  logger.info('Gemini proxy response sent', {
97
112
  model: this.defaultModel,
98
- usage: anthropicRes.usage
113
+ usage: anthropicRes.usage,
114
+ contentBlocks: anthropicRes.content?.length,
115
+ hasText: anthropicRes.content?.some((c) => c.type === 'text'),
116
+ firstContent: anthropicRes.content?.[0]
99
117
  });
100
118
  res.json(anthropicRes);
101
119
  }
@@ -284,21 +302,33 @@ The system will automatically execute these commands and provide results.
284
302
  convertGeminiToAnthropic(geminiRes) {
285
303
  const candidate = geminiRes.candidates?.[0];
286
304
  if (!candidate) {
305
+ logger.error('No candidates in Gemini response', { geminiRes });
287
306
  throw new Error('No candidates in Gemini response');
288
307
  }
289
308
  const content = candidate.content;
290
309
  const parts = content?.parts || [];
310
+ logger.info('Converting Gemini to Anthropic', {
311
+ hasParts: !!parts,
312
+ partsCount: parts.length,
313
+ partTypes: parts.map((p) => Object.keys(p))
314
+ });
291
315
  // Extract text and function calls
292
316
  let rawText = '';
293
317
  const functionCalls = [];
294
318
  for (const part of parts) {
295
319
  if (part.text) {
296
320
  rawText += part.text;
321
+ logger.info('Found text in part', { textLength: part.text.length, textPreview: part.text.substring(0, 100) });
297
322
  }
298
323
  if (part.functionCall) {
299
324
  functionCalls.push(part.functionCall);
300
325
  }
301
326
  }
327
+ logger.info('Extracted content from Gemini', {
328
+ rawTextLength: rawText.length,
329
+ functionCallsCount: functionCalls.length,
330
+ rawTextPreview: rawText.substring(0, 200)
331
+ });
302
332
  // Parse structured commands from Gemini's text response
303
333
  const { cleanText, toolUses } = this.parseStructuredCommands(rawText);
304
334
  // Build content array with text and tool uses
@@ -345,27 +375,32 @@ The system will automatically execute these commands and provide results.
345
375
  };
346
376
  }
347
377
  convertGeminiStreamToAnthropic(chunk) {
348
- // Gemini streaming returns newline-delimited JSON
378
+ // Gemini streaming returns Server-Sent Events format: "data: {json}"
349
379
  const lines = chunk.split('\n').filter(line => line.trim());
350
380
  const anthropicChunks = [];
351
381
  for (const line of lines) {
352
382
  try {
353
- const parsed = JSON.parse(line);
354
- const candidate = parsed.candidates?.[0];
355
- const text = candidate?.content?.parts?.[0]?.text;
356
- if (text) {
357
- anthropicChunks.push(`event: content_block_delta\ndata: ${JSON.stringify({
358
- type: 'content_block_delta',
359
- delta: { type: 'text_delta', text }
360
- })}\n\n`);
361
- }
362
- // Check for finish
363
- if (candidate?.finishReason) {
364
- anthropicChunks.push('event: message_stop\ndata: {}\n\n');
383
+ // Parse SSE format: "data: {json}"
384
+ if (line.startsWith('data: ')) {
385
+ const jsonStr = line.substring(6); // Remove "data: " prefix
386
+ const parsed = JSON.parse(jsonStr);
387
+ const candidate = parsed.candidates?.[0];
388
+ const text = candidate?.content?.parts?.[0]?.text;
389
+ if (text) {
390
+ anthropicChunks.push(`event: content_block_delta\ndata: ${JSON.stringify({
391
+ type: 'content_block_delta',
392
+ delta: { type: 'text_delta', text }
393
+ })}\n\n`);
394
+ }
395
+ // Check for finish
396
+ if (candidate?.finishReason) {
397
+ anthropicChunks.push('event: message_stop\ndata: {}\n\n');
398
+ }
365
399
  }
366
400
  }
367
401
  catch (e) {
368
402
  // Ignore parse errors
403
+ logger.debug('Failed to parse Gemini stream chunk', { line, error: e.message });
369
404
  }
370
405
  }
371
406
  return anthropicChunks.join('');
@@ -0,0 +1,439 @@
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
+ constructor(config) {
7
+ this.app = express();
8
+ this.geminiApiKey = config.geminiApiKey;
9
+ this.geminiBaseUrl = config.geminiBaseUrl || 'https://generativelanguage.googleapis.com/v1beta';
10
+ this.defaultModel = config.defaultModel || 'gemini-2.0-flash-exp';
11
+ this.setupMiddleware();
12
+ this.setupRoutes();
13
+ }
14
+ setupMiddleware() {
15
+ // Parse JSON bodies
16
+ this.app.use(express.json({ limit: '50mb' }));
17
+ // Logging middleware
18
+ this.app.use((req, res, next) => {
19
+ logger.debug('Gemini proxy request', {
20
+ method: req.method,
21
+ path: req.path,
22
+ headers: Object.keys(req.headers)
23
+ });
24
+ next();
25
+ });
26
+ }
27
+ setupRoutes() {
28
+ // Health check
29
+ this.app.get('/health', (req, res) => {
30
+ res.json({ status: 'ok', service: 'anthropic-to-gemini-proxy' });
31
+ });
32
+ // Anthropic Messages API → Gemini generateContent
33
+ this.app.post('/v1/messages', async (req, res) => {
34
+ try {
35
+ const anthropicReq = req.body;
36
+ // Convert Anthropic format to Gemini format
37
+ const geminiReq = this.convertAnthropicToGemini(anthropicReq);
38
+ logger.info('Converting Anthropic request to Gemini', {
39
+ anthropicModel: anthropicReq.model,
40
+ geminiModel: this.defaultModel,
41
+ messageCount: geminiReq.contents.length,
42
+ stream: anthropicReq.stream,
43
+ apiKeyPresent: !!this.geminiApiKey,
44
+ apiKeyPrefix: this.geminiApiKey?.substring(0, 10)
45
+ });
46
+ // Determine endpoint based on streaming
47
+ const endpoint = anthropicReq.stream ? 'streamGenerateContent' : 'generateContent';
48
+ const url = `${this.geminiBaseUrl}/models/${this.defaultModel}:${endpoint}?key=${this.geminiApiKey}`;
49
+ // Forward to Gemini
50
+ const response = await fetch(url, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json'
54
+ },
55
+ body: JSON.stringify(geminiReq)
56
+ });
57
+ if (!response.ok) {
58
+ const error = await response.text();
59
+ logger.error('Gemini API error', { status: response.status, error });
60
+ return res.status(response.status).json({
61
+ error: {
62
+ type: 'api_error',
63
+ message: error
64
+ }
65
+ });
66
+ }
67
+ // Handle streaming vs non-streaming
68
+ if (anthropicReq.stream) {
69
+ // Stream response
70
+ res.setHeader('Content-Type', 'text/event-stream');
71
+ res.setHeader('Cache-Control', 'no-cache');
72
+ res.setHeader('Connection', 'keep-alive');
73
+ const reader = response.body?.getReader();
74
+ if (!reader) {
75
+ throw new Error('No response body');
76
+ }
77
+ const decoder = new TextDecoder();
78
+ let chunkCount = 0;
79
+ while (true) {
80
+ const { done, value } = await reader.read();
81
+ if (done)
82
+ break;
83
+ const chunk = decoder.decode(value);
84
+ chunkCount++;
85
+ logger.info('Gemini stream chunk received', { chunkCount, chunkLength: chunk.length, chunkPreview: chunk.substring(0, 200) });
86
+ const anthropicChunk = this.convertGeminiStreamToAnthropic(chunk);
87
+ logger.info('Anthropic stream chunk generated', { chunkCount, anthropicLength: anthropicChunk.length, anthropicPreview: anthropicChunk.substring(0, 200) });
88
+ res.write(anthropicChunk);
89
+ }
90
+ logger.info('Gemini stream complete', { totalChunks: chunkCount });
91
+ res.end();
92
+ }
93
+ else {
94
+ // Non-streaming response
95
+ const geminiRes = await response.json();
96
+ // DEBUG: Log raw Gemini response
97
+ logger.info('Raw Gemini API response', {
98
+ hasResponse: !!geminiRes,
99
+ hasCandidates: !!geminiRes.candidates,
100
+ candidatesLength: geminiRes.candidates?.length,
101
+ firstCandidate: geminiRes.candidates?.[0],
102
+ fullResponse: JSON.stringify(geminiRes).substring(0, 500)
103
+ });
104
+ const anthropicRes = this.convertGeminiToAnthropic(geminiRes);
105
+ logger.info('Gemini proxy response sent', {
106
+ model: this.defaultModel,
107
+ usage: anthropicRes.usage,
108
+ contentBlocks: anthropicRes.content?.length,
109
+ hasText: anthropicRes.content?.some((c) => c.type === 'text'),
110
+ firstContent: anthropicRes.content?.[0]
111
+ });
112
+ res.json(anthropicRes);
113
+ }
114
+ }
115
+ catch (error) {
116
+ logger.error('Gemini proxy error', { error: error.message, stack: error.stack });
117
+ res.status(500).json({
118
+ error: {
119
+ type: 'proxy_error',
120
+ message: error.message
121
+ }
122
+ });
123
+ }
124
+ });
125
+ // Fallback for other Anthropic API endpoints
126
+ this.app.use((req, res) => {
127
+ logger.warn('Unsupported endpoint', { path: req.path, method: req.method });
128
+ res.status(404).json({
129
+ error: {
130
+ type: 'not_found',
131
+ message: `Endpoint ${req.path} not supported by Gemini proxy`
132
+ }
133
+ });
134
+ });
135
+ }
136
+ convertAnthropicToGemini(anthropicReq) {
137
+ const contents = [];
138
+ // Add system message as first user message if present
139
+ // Gemini doesn't have a dedicated system role, so we prepend it to the first user message
140
+ let systemPrefix = '';
141
+ if (anthropicReq.system) {
142
+ systemPrefix = `System: ${anthropicReq.system}\n\n`;
143
+ }
144
+ // Add tool instructions for Gemini to understand file operations
145
+ // Since Gemini doesn't have native tool calling, we instruct it to use structured XML-like commands
146
+ const toolInstructions = `
147
+ IMPORTANT: You have access to file system operations through structured commands. Use these exact formats:
148
+
149
+ <file_write path="filename.ext">
150
+ content here
151
+ </file_write>
152
+
153
+ <file_read path="filename.ext"/>
154
+
155
+ <bash_command>
156
+ command here
157
+ </bash_command>
158
+
159
+ When you need to create, edit, or read files, use these structured commands in your response.
160
+ The system will automatically execute these commands and provide results.
161
+
162
+ `;
163
+ // Prepend tool instructions to system prompt
164
+ if (systemPrefix) {
165
+ systemPrefix = toolInstructions + systemPrefix;
166
+ }
167
+ else {
168
+ systemPrefix = toolInstructions;
169
+ }
170
+ // Convert Anthropic messages to Gemini format
171
+ for (let i = 0; i < anthropicReq.messages.length; i++) {
172
+ const msg = anthropicReq.messages[i];
173
+ let text;
174
+ if (typeof msg.content === 'string') {
175
+ text = msg.content;
176
+ }
177
+ else if (Array.isArray(msg.content)) {
178
+ // Extract text from content blocks
179
+ text = msg.content
180
+ .filter(block => block.type === 'text')
181
+ .map(block => block.text)
182
+ .join('\n');
183
+ }
184
+ else {
185
+ text = '';
186
+ }
187
+ // Add system prefix to first user message
188
+ if (i === 0 && msg.role === 'user' && systemPrefix) {
189
+ text = systemPrefix + text;
190
+ }
191
+ contents.push({
192
+ role: msg.role === 'assistant' ? 'model' : 'user',
193
+ parts: [{ text }]
194
+ });
195
+ }
196
+ const geminiReq = {
197
+ contents
198
+ };
199
+ // Add generation config if temperature or max_tokens specified
200
+ if (anthropicReq.temperature !== undefined || anthropicReq.max_tokens !== undefined) {
201
+ geminiReq.generationConfig = {};
202
+ if (anthropicReq.temperature !== undefined) {
203
+ geminiReq.generationConfig.temperature = anthropicReq.temperature;
204
+ }
205
+ if (anthropicReq.max_tokens !== undefined) {
206
+ geminiReq.generationConfig.maxOutputTokens = anthropicReq.max_tokens;
207
+ }
208
+ }
209
+ // Convert MCP/Anthropic tools to Gemini tools format
210
+ if (anthropicReq.tools && anthropicReq.tools.length > 0) {
211
+ geminiReq.tools = [{
212
+ functionDeclarations: anthropicReq.tools.map(tool => {
213
+ // Clean schema: Remove $schema and additionalProperties fields that Gemini doesn't support
214
+ const cleanSchema = (schema) => {
215
+ if (!schema || typeof schema !== 'object')
216
+ return schema;
217
+ const { $schema, additionalProperties, ...rest } = schema;
218
+ const cleaned = { ...rest };
219
+ // Recursively clean nested objects
220
+ if (cleaned.properties) {
221
+ cleaned.properties = Object.fromEntries(Object.entries(cleaned.properties).map(([key, value]) => [
222
+ key,
223
+ cleanSchema(value)
224
+ ]));
225
+ }
226
+ // Clean items if present
227
+ if (cleaned.items) {
228
+ cleaned.items = cleanSchema(cleaned.items);
229
+ }
230
+ return cleaned;
231
+ };
232
+ return {
233
+ name: tool.name,
234
+ description: tool.description || '',
235
+ parameters: cleanSchema(tool.input_schema) || {
236
+ type: 'object',
237
+ properties: {},
238
+ required: []
239
+ }
240
+ };
241
+ })
242
+ }];
243
+ logger.info('Forwarding MCP tools to Gemini', {
244
+ toolCount: anthropicReq.tools.length,
245
+ toolNames: anthropicReq.tools.map(t => t.name)
246
+ });
247
+ }
248
+ return geminiReq;
249
+ }
250
+ parseStructuredCommands(text) {
251
+ const toolUses = [];
252
+ let cleanText = text;
253
+ // Parse file_write commands
254
+ const fileWriteRegex = /<file_write path="([^"]+)">([\s\S]*?)<\/file_write>/g;
255
+ let match;
256
+ while ((match = fileWriteRegex.exec(text)) !== null) {
257
+ toolUses.push({
258
+ type: 'tool_use',
259
+ id: `tool_${Date.now()}_${toolUses.length}`,
260
+ name: 'Write',
261
+ input: {
262
+ file_path: match[1],
263
+ content: match[2].trim()
264
+ }
265
+ });
266
+ cleanText = cleanText.replace(match[0], `[File written: ${match[1]}]`);
267
+ }
268
+ // Parse file_read commands
269
+ const fileReadRegex = /<file_read path="([^"]+)"\/>/g;
270
+ while ((match = fileReadRegex.exec(text)) !== null) {
271
+ toolUses.push({
272
+ type: 'tool_use',
273
+ id: `tool_${Date.now()}_${toolUses.length}`,
274
+ name: 'Read',
275
+ input: {
276
+ file_path: match[1]
277
+ }
278
+ });
279
+ cleanText = cleanText.replace(match[0], `[Reading file: ${match[1]}]`);
280
+ }
281
+ // Parse bash commands
282
+ const bashRegex = /<bash_command>([\s\S]*?)<\/bash_command>/g;
283
+ while ((match = bashRegex.exec(text)) !== null) {
284
+ toolUses.push({
285
+ type: 'tool_use',
286
+ id: `tool_${Date.now()}_${toolUses.length}`,
287
+ name: 'Bash',
288
+ input: {
289
+ command: match[1].trim()
290
+ }
291
+ });
292
+ cleanText = cleanText.replace(match[0], `[Executing: ${match[1].trim()}]`);
293
+ }
294
+ return { cleanText: cleanText.trim(), toolUses };
295
+ }
296
+ convertGeminiToAnthropic(geminiRes) {
297
+ const candidate = geminiRes.candidates?.[0];
298
+ if (!candidate) {
299
+ logger.error('No candidates in Gemini response', { geminiRes });
300
+ throw new Error('No candidates in Gemini response');
301
+ }
302
+ const content = candidate.content;
303
+ const parts = content?.parts || [];
304
+ logger.info('Converting Gemini to Anthropic', {
305
+ hasParts: !!parts,
306
+ partsCount: parts.length,
307
+ partTypes: parts.map((p) => Object.keys(p))
308
+ });
309
+ // Extract text and function calls
310
+ let rawText = '';
311
+ const functionCalls = [];
312
+ for (const part of parts) {
313
+ if (part.text) {
314
+ rawText += part.text;
315
+ logger.info('Found text in part', { textLength: part.text.length, textPreview: part.text.substring(0, 100) });
316
+ }
317
+ if (part.functionCall) {
318
+ functionCalls.push(part.functionCall);
319
+ }
320
+ }
321
+ logger.info('Extracted content from Gemini', {
322
+ rawTextLength: rawText.length,
323
+ functionCallsCount: functionCalls.length,
324
+ rawTextPreview: rawText.substring(0, 200)
325
+ });
326
+ // Parse structured commands from Gemini's text response
327
+ const { cleanText, toolUses } = this.parseStructuredCommands(rawText);
328
+ // Build content array with text and tool uses
329
+ const contentBlocks = [];
330
+ if (cleanText) {
331
+ contentBlocks.push({
332
+ type: 'text',
333
+ text: cleanText
334
+ });
335
+ }
336
+ // Add tool uses from structured commands
337
+ contentBlocks.push(...toolUses);
338
+ // Add tool uses from Gemini function calls (MCP tools)
339
+ if (functionCalls.length > 0) {
340
+ for (const functionCall of functionCalls) {
341
+ contentBlocks.push({
342
+ type: 'tool_use',
343
+ id: `tool_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
344
+ name: functionCall.name,
345
+ input: functionCall.args || {}
346
+ });
347
+ }
348
+ logger.info('Converted Gemini function calls to Anthropic format', {
349
+ functionCallCount: functionCalls.length,
350
+ functionNames: functionCalls.map((fc) => fc.name)
351
+ });
352
+ }
353
+ return {
354
+ id: `msg_${Date.now()}`,
355
+ type: 'message',
356
+ role: 'assistant',
357
+ model: this.defaultModel,
358
+ content: contentBlocks.length > 0 ? contentBlocks : [
359
+ {
360
+ type: 'text',
361
+ text: rawText
362
+ }
363
+ ],
364
+ stop_reason: this.mapFinishReason(candidate.finishReason),
365
+ usage: {
366
+ input_tokens: geminiRes.usageMetadata?.promptTokenCount || 0,
367
+ output_tokens: geminiRes.usageMetadata?.candidatesTokenCount || 0
368
+ }
369
+ };
370
+ }
371
+ convertGeminiStreamToAnthropic(chunk) {
372
+ // Gemini streaming returns Server-Sent Events format: "data: {json}"
373
+ const lines = chunk.split('\n').filter(line => line.trim());
374
+ const anthropicChunks = [];
375
+ for (const line of lines) {
376
+ try {
377
+ // Parse SSE format: "data: {json}"
378
+ if (line.startsWith('data: ')) {
379
+ const jsonStr = line.substring(6); // Remove "data: " prefix
380
+ const parsed = JSON.parse(jsonStr);
381
+ const candidate = parsed.candidates?.[0];
382
+ const text = candidate?.content?.parts?.[0]?.text;
383
+ if (text) {
384
+ anthropicChunks.push(`event: content_block_delta\ndata: ${JSON.stringify({
385
+ type: 'content_block_delta',
386
+ delta: { type: 'text_delta', text }
387
+ })}\n\n`);
388
+ }
389
+ // Check for finish
390
+ if (candidate?.finishReason) {
391
+ anthropicChunks.push('event: message_stop\ndata: {}\n\n');
392
+ }
393
+ }
394
+ }
395
+ catch (e) {
396
+ // Ignore parse errors
397
+ logger.debug('Failed to parse Gemini stream chunk', { line, error: e.message });
398
+ }
399
+ }
400
+ return anthropicChunks.join('');
401
+ }
402
+ mapFinishReason(reason) {
403
+ const mapping = {
404
+ 'STOP': 'end_turn',
405
+ 'MAX_TOKENS': 'max_tokens',
406
+ 'SAFETY': 'stop_sequence',
407
+ 'RECITATION': 'stop_sequence',
408
+ 'OTHER': 'end_turn'
409
+ };
410
+ return mapping[reason || 'STOP'] || 'end_turn';
411
+ }
412
+ start(port) {
413
+ this.app.listen(port, () => {
414
+ logger.info('Anthropic to Gemini proxy started', {
415
+ port,
416
+ geminiBaseUrl: this.geminiBaseUrl,
417
+ defaultModel: this.defaultModel
418
+ });
419
+ console.log(`\nāœ… Gemini Proxy running at http://localhost:${port}`);
420
+ console.log(` Gemini Base URL: ${this.geminiBaseUrl}`);
421
+ console.log(` Default Model: ${this.defaultModel}\n`);
422
+ });
423
+ }
424
+ }
425
+ // CLI entry point
426
+ if (import.meta.url === `file://${process.argv[1]}`) {
427
+ const port = parseInt(process.env.PORT || '3001');
428
+ const geminiApiKey = process.env.GOOGLE_GEMINI_API_KEY;
429
+ if (!geminiApiKey) {
430
+ console.error('āŒ Error: GOOGLE_GEMINI_API_KEY environment variable required');
431
+ process.exit(1);
432
+ }
433
+ const proxy = new AnthropicToGeminiProxy({
434
+ geminiApiKey,
435
+ geminiBaseUrl: process.env.GEMINI_BASE_URL,
436
+ defaultModel: process.env.COMPLETION_MODEL || process.env.REASONING_MODEL
437
+ });
438
+ proxy.start(port);
439
+ }
@@ -0,0 +1,59 @@
1
+ class Logger {
2
+ constructor() {
3
+ this.context = {};
4
+ }
5
+ setContext(ctx) {
6
+ this.context = { ...this.context, ...ctx };
7
+ }
8
+ log(level, message, data) {
9
+ // Skip all logs if QUIET mode is enabled (unless it's an error)
10
+ if (process.env.QUIET === 'true' && level !== 'error') {
11
+ return;
12
+ }
13
+ const timestamp = new Date().toISOString();
14
+ const logEntry = {
15
+ timestamp,
16
+ level,
17
+ message,
18
+ ...this.context,
19
+ ...data
20
+ };
21
+ // Structured JSON logging for production
22
+ if (process.env.NODE_ENV === 'production') {
23
+ console.log(JSON.stringify(logEntry));
24
+ }
25
+ else {
26
+ // Human-readable for development
27
+ const prefix = `[${timestamp}] ${level.toUpperCase()}`;
28
+ const contextStr = Object.keys({ ...this.context, ...data }).length > 0
29
+ ? ` ${JSON.stringify({ ...this.context, ...data })}`
30
+ : '';
31
+ console.log(`${prefix}: ${message}${contextStr}`);
32
+ }
33
+ }
34
+ debug(message, data) {
35
+ // Skip debug logs unless DEBUG or VERBOSE environment variable is set
36
+ if (!process.env.DEBUG && !process.env.VERBOSE) {
37
+ return;
38
+ }
39
+ this.log('debug', message, data);
40
+ }
41
+ info(message, data) {
42
+ // Skip info logs unless VERBOSE is set
43
+ if (!process.env.DEBUG && !process.env.VERBOSE) {
44
+ return;
45
+ }
46
+ this.log('info', message, data);
47
+ }
48
+ warn(message, data) {
49
+ // Skip warnings unless VERBOSE is set
50
+ if (!process.env.DEBUG && !process.env.VERBOSE) {
51
+ return;
52
+ }
53
+ this.log('warn', message, data);
54
+ }
55
+ error(message, data) {
56
+ this.log('error', message, data);
57
+ }
58
+ }
59
+ export const logger = new Logger();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-flow",
3
- "version": "1.9.2",
3
+ "version": "1.9.3",
4
4
  "description": "Production-ready AI agent orchestration platform with 66 specialized agents, 213 MCP tools, ReasoningBank learning memory, and autonomous multi-agent swarms. Built by @ruvnet with Claude Agent SDK, neural networks, memory persistence, GitHub integration, and distributed consensus protocols.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -258,7 +258,7 @@ export function log(message) {
258
258
  wasm.log(ptr0, len0);
259
259
  }
260
260
 
261
- function __wbg_adapter_4(arg0, arg1, arg2) {
261
+ function __wbg_adapter_6(arg0, arg1, arg2) {
262
262
  wasm.__wbindgen_export_5(arg0, arg1, addHeapObject(arg2));
263
263
  }
264
264
 
@@ -540,7 +540,7 @@ export function __wbindgen_cast_2241b6af4c4b2941(arg0, arg1) {
540
540
 
541
541
  export function __wbindgen_cast_8eb6fd44e7238d11(arg0, arg1) {
542
542
  // Cast intrinsic for `Closure(Closure { dtor_idx: 62, function: Function { arguments: [Externref], shim_idx: 63, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
543
- const ret = makeMutClosure(arg0, arg1, 62, __wbg_adapter_4);
543
+ const ret = makeMutClosure(arg0, arg1, 62, __wbg_adapter_6);
544
544
  return addHeapObject(ret);
545
545
  };
546
546