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.
- package/dist/agents/claudeAgent.js +113 -51
- package/dist/agents/directApiAgent.js +22 -4
- package/dist/cli-proxy.js +0 -0
- package/dist/proxy/anthropic-to-gemini.js +345 -0
- package/dist/proxy/anthropic-to-openrouter.js +82 -8
- package/dist/proxy/provider-instructions.js +198 -0
- package/docs/.claude-flow/metrics/agent-metrics.json +1 -0
- package/docs/.claude-flow/metrics/performance.json +9 -0
- package/docs/.claude-flow/metrics/task-metrics.json +10 -0
- package/docs/FINAL_SDK_VALIDATION.md +328 -0
- package/docs/MCP_INTEGRATION_SUCCESS.md +305 -0
- package/docs/OPTIMIZATION_SUMMARY.md +181 -0
- package/docs/PROVIDER_INSTRUCTION_OPTIMIZATION.md +139 -0
- package/docs/TOOL_INSTRUCTION_ENHANCEMENT.md +200 -0
- package/docs/TOP20_MODELS_MATRIX.md +80 -0
- package/docs/VALIDATION_COMPLETE.md +178 -0
- package/docs/archived/HOTFIX_1.1.7.md +133 -0
- package/docs/validation/PROXY_VALIDATION.md +239 -0
- package/docs/validation/README_SDK_VALIDATION.md +356 -0
- package/package.json +1 -1
- package/docs/CHANGELOG.md +0 -155
|
@@ -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
|
-
//
|
|
66
|
-
// The SDK
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
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
|
-
|
|
100
|
+
hasApiKey: !!envOverrides.ANTHROPIC_API_KEY || !!process.env.ANTHROPIC_API_KEY,
|
|
101
|
+
hasBaseURL: !!envOverrides.ANTHROPIC_BASE_URL
|
|
75
102
|
});
|
|
76
103
|
try {
|
|
77
|
-
//
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|