agentic-flow 1.3.0 → 1.3.1

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.
@@ -0,0 +1,627 @@
1
+ // Anthropic to Requesty Proxy Server
2
+ // Converts Anthropic API format to Requesty format
3
+ import express from 'express';
4
+ import { logger } from '../utils/logger.js';
5
+ import { getMaxTokensForModel } from './provider-instructions.js';
6
+ import { detectModelCapabilities } from '../utils/modelCapabilities.js';
7
+ import { ToolEmulator, executeEmulation } from './tool-emulation.js';
8
+ export class AnthropicToRequestyProxy {
9
+ app;
10
+ requestyApiKey;
11
+ requestyBaseUrl;
12
+ defaultModel;
13
+ capabilities;
14
+ constructor(config) {
15
+ this.app = express();
16
+ this.requestyApiKey = config.requestyApiKey;
17
+ this.requestyBaseUrl = config.requestyBaseUrl || 'https://router.requesty.ai/v1';
18
+ this.defaultModel = config.defaultModel || 'deepseek/deepseek-chat';
19
+ this.capabilities = config.capabilities;
20
+ // Debug logging
21
+ if (this.capabilities) {
22
+ logger.info('Proxy initialized with capabilities', {
23
+ model: this.defaultModel,
24
+ requiresEmulation: this.capabilities.requiresEmulation,
25
+ strategy: this.capabilities.emulationStrategy
26
+ });
27
+ }
28
+ this.setupMiddleware();
29
+ this.setupRoutes();
30
+ }
31
+ setupMiddleware() {
32
+ // Parse JSON bodies
33
+ this.app.use(express.json({ limit: '50mb' }));
34
+ // Logging middleware
35
+ this.app.use((req, res, next) => {
36
+ logger.debug('Proxy request', {
37
+ method: req.method,
38
+ path: req.path,
39
+ headers: Object.keys(req.headers)
40
+ });
41
+ next();
42
+ });
43
+ }
44
+ setupRoutes() {
45
+ // Health check
46
+ this.app.get('/health', (req, res) => {
47
+ res.json({ status: 'ok', service: 'anthropic-to-requesty-proxy' });
48
+ });
49
+ // Anthropic Messages API → Requesty Chat Completions
50
+ this.app.post('/v1/messages', async (req, res) => {
51
+ try {
52
+ const anthropicReq = req.body;
53
+ // VERBOSE LOGGING: Log incoming Anthropic request
54
+ // Handle system prompt which can be string OR array of content blocks
55
+ const systemPreview = typeof anthropicReq.system === 'string'
56
+ ? anthropicReq.system.substring(0, 200)
57
+ : Array.isArray(anthropicReq.system)
58
+ ? JSON.stringify(anthropicReq.system).substring(0, 200)
59
+ : undefined;
60
+ logger.info('=== INCOMING ANTHROPIC REQUEST ===', {
61
+ model: anthropicReq.model,
62
+ systemPrompt: systemPreview,
63
+ systemType: typeof anthropicReq.system,
64
+ messageCount: anthropicReq.messages?.length,
65
+ toolCount: anthropicReq.tools?.length || 0,
66
+ toolNames: anthropicReq.tools?.map(t => t.name) || [],
67
+ maxTokens: anthropicReq.max_tokens,
68
+ temperature: anthropicReq.temperature,
69
+ stream: anthropicReq.stream
70
+ });
71
+ // Log first user message for debugging
72
+ if (anthropicReq.messages && anthropicReq.messages.length > 0) {
73
+ const firstMsg = anthropicReq.messages[0];
74
+ logger.info('First user message:', {
75
+ role: firstMsg.role,
76
+ contentPreview: typeof firstMsg.content === 'string'
77
+ ? firstMsg.content.substring(0, 200)
78
+ : JSON.stringify(firstMsg.content).substring(0, 200)
79
+ });
80
+ }
81
+ // Route to appropriate handler based on capabilities
82
+ const result = await this.handleRequest(anthropicReq, res);
83
+ if (result) {
84
+ res.json(result);
85
+ }
86
+ }
87
+ catch (error) {
88
+ logger.error('Proxy error', { error: error.message, stack: error.stack });
89
+ res.status(500).json({
90
+ error: {
91
+ type: 'proxy_error',
92
+ message: error.message
93
+ }
94
+ });
95
+ }
96
+ });
97
+ // Fallback for other Anthropic API endpoints
98
+ this.app.use((req, res) => {
99
+ logger.warn('Unsupported endpoint', { path: req.path, method: req.method });
100
+ res.status(404).json({
101
+ error: {
102
+ type: 'not_found',
103
+ message: `Endpoint ${req.path} not supported by proxy`
104
+ }
105
+ });
106
+ });
107
+ }
108
+ async handleRequest(anthropicReq, res) {
109
+ let model = anthropicReq.model || this.defaultModel;
110
+ // If SDK is requesting a Claude model but we're using Requesty with a different default,
111
+ // override to use the CLI-specified model
112
+ if (model.startsWith('claude-') && this.defaultModel && !this.defaultModel.startsWith('claude-')) {
113
+ logger.info(`Overriding SDK Claude model ${model} with CLI-specified ${this.defaultModel}`);
114
+ model = this.defaultModel;
115
+ anthropicReq.model = model;
116
+ }
117
+ const capabilities = this.capabilities || detectModelCapabilities(model);
118
+ // Check if emulation is required
119
+ if (capabilities.requiresEmulation && anthropicReq.tools && anthropicReq.tools.length > 0) {
120
+ logger.info(`Using tool emulation for model: ${model}`);
121
+ return this.handleEmulatedRequest(anthropicReq, capabilities);
122
+ }
123
+ return this.handleNativeRequest(anthropicReq, res);
124
+ }
125
+ async handleNativeRequest(anthropicReq, res) {
126
+ // Convert Anthropic format to OpenAI format
127
+ const openaiReq = this.convertAnthropicToOpenAI(anthropicReq);
128
+ // VERBOSE LOGGING: Log converted OpenAI request
129
+ logger.info('=== CONVERTED OPENAI REQUEST ===', {
130
+ anthropicModel: anthropicReq.model,
131
+ openaiModel: openaiReq.model,
132
+ messageCount: openaiReq.messages.length,
133
+ systemPrompt: openaiReq.messages[0]?.content?.substring(0, 300),
134
+ toolCount: openaiReq.tools?.length || 0,
135
+ toolNames: openaiReq.tools?.map(t => t.function.name) || [],
136
+ maxTokens: openaiReq.max_tokens,
137
+ apiKeyPresent: !!this.requestyApiKey,
138
+ apiKeyPrefix: this.requestyApiKey?.substring(0, 10)
139
+ });
140
+ // Forward to Requesty
141
+ const response = await fetch(`${this.requestyBaseUrl}/chat/completions`, {
142
+ method: 'POST',
143
+ headers: {
144
+ 'Authorization': `Bearer ${this.requestyApiKey}`,
145
+ 'Content-Type': 'application/json',
146
+ 'HTTP-Referer': 'https://github.com/ruvnet/agentic-flow',
147
+ 'X-Title': 'Agentic Flow'
148
+ },
149
+ body: JSON.stringify(openaiReq)
150
+ });
151
+ if (!response.ok) {
152
+ const error = await response.text();
153
+ logger.error('Requesty API error', { status: response.status, error });
154
+ res.status(response.status).json({
155
+ error: {
156
+ type: 'api_error',
157
+ message: error
158
+ }
159
+ });
160
+ return null;
161
+ }
162
+ // VERBOSE LOGGING: Log Requesty response status
163
+ logger.info('=== REQUESTY RESPONSE RECEIVED ===', {
164
+ status: response.status,
165
+ statusText: response.statusText,
166
+ headers: Object.fromEntries(response.headers.entries())
167
+ });
168
+ // Handle streaming vs non-streaming
169
+ if (anthropicReq.stream) {
170
+ logger.info('Handling streaming response...');
171
+ // Stream response
172
+ res.setHeader('Content-Type', 'text/event-stream');
173
+ res.setHeader('Cache-Control', 'no-cache');
174
+ res.setHeader('Connection', 'keep-alive');
175
+ const reader = response.body?.getReader();
176
+ if (!reader) {
177
+ throw new Error('No response body');
178
+ }
179
+ const decoder = new TextDecoder();
180
+ while (true) {
181
+ const { done, value } = await reader.read();
182
+ if (done)
183
+ break;
184
+ const chunk = decoder.decode(value);
185
+ const anthropicChunk = this.convertOpenAIStreamToAnthropic(chunk);
186
+ res.write(anthropicChunk);
187
+ }
188
+ res.end();
189
+ return null; // Already sent response
190
+ }
191
+ else {
192
+ logger.info('Handling non-streaming response...');
193
+ // Non-streaming response
194
+ const openaiRes = await response.json();
195
+ // VERBOSE LOGGING: Log raw OpenAI response
196
+ logger.info('=== RAW OPENAI RESPONSE ===', {
197
+ id: openaiRes.id,
198
+ model: openaiRes.model,
199
+ choices: openaiRes.choices?.length,
200
+ finishReason: openaiRes.choices?.[0]?.finish_reason,
201
+ hasToolCalls: !!(openaiRes.choices?.[0]?.message?.tool_calls),
202
+ toolCallCount: openaiRes.choices?.[0]?.message?.tool_calls?.length || 0,
203
+ toolCallNames: openaiRes.choices?.[0]?.message?.tool_calls?.map((tc) => tc.function.name) || [],
204
+ contentPreview: openaiRes.choices?.[0]?.message?.content?.substring(0, 300),
205
+ usage: openaiRes.usage
206
+ });
207
+ const anthropicRes = this.convertOpenAIToAnthropic(openaiRes);
208
+ // VERBOSE LOGGING: Log converted Anthropic response
209
+ logger.info('=== CONVERTED ANTHROPIC RESPONSE ===', {
210
+ id: anthropicRes.id,
211
+ model: anthropicRes.model,
212
+ role: anthropicRes.role,
213
+ stopReason: anthropicRes.stop_reason,
214
+ contentBlocks: anthropicRes.content?.length,
215
+ contentTypes: anthropicRes.content?.map((c) => c.type),
216
+ toolUseCount: anthropicRes.content?.filter((c) => c.type === 'tool_use').length,
217
+ textPreview: anthropicRes.content?.find((c) => c.type === 'text')?.text?.substring(0, 200),
218
+ usage: anthropicRes.usage
219
+ });
220
+ return anthropicRes;
221
+ }
222
+ }
223
+ async handleEmulatedRequest(anthropicReq, capabilities) {
224
+ const emulator = new ToolEmulator(anthropicReq.tools || [], capabilities.emulationStrategy);
225
+ const lastMessage = anthropicReq.messages[anthropicReq.messages.length - 1];
226
+ const userMessage = typeof lastMessage.content === 'string'
227
+ ? lastMessage.content
228
+ : (lastMessage.content.find(c => c.type === 'text')?.text || '');
229
+ const result = await executeEmulation(emulator, userMessage, async (prompt) => {
230
+ // Call model with emulation prompt
231
+ // Cap max_tokens at 8192 for OpenAI models via Requesty
232
+ let maxTokens = anthropicReq.max_tokens;
233
+ if (maxTokens && maxTokens > 8192) {
234
+ maxTokens = 8192;
235
+ }
236
+ const openaiReq = {
237
+ model: anthropicReq.model || this.defaultModel,
238
+ messages: [{ role: 'user', content: prompt }],
239
+ temperature: anthropicReq.temperature,
240
+ max_tokens: maxTokens
241
+ };
242
+ const response = await this.callRequesty(openaiReq);
243
+ return response.choices[0].message.content;
244
+ }, async (toolCall) => {
245
+ logger.warn(`Tool execution not yet implemented: ${toolCall.name}`);
246
+ return { error: 'Tool execution not implemented in Phase 2' };
247
+ }, { maxIterations: 5, verbose: process.env.VERBOSE === 'true' });
248
+ return {
249
+ id: `emulated_${Date.now()}`,
250
+ type: 'message',
251
+ role: 'assistant',
252
+ content: [{ type: 'text', text: result.finalAnswer || 'No response' }],
253
+ model: anthropicReq.model || this.defaultModel,
254
+ stop_reason: 'end_turn',
255
+ usage: { input_tokens: 0, output_tokens: 0 }
256
+ };
257
+ }
258
+ async callRequesty(openaiReq) {
259
+ const response = await fetch(`${this.requestyBaseUrl}/chat/completions`, {
260
+ method: 'POST',
261
+ headers: {
262
+ 'Authorization': `Bearer ${this.requestyApiKey}`,
263
+ 'Content-Type': 'application/json',
264
+ 'HTTP-Referer': 'https://github.com/ruvnet/agentic-flow',
265
+ 'X-Title': 'Agentic Flow'
266
+ },
267
+ body: JSON.stringify(openaiReq)
268
+ });
269
+ if (!response.ok) {
270
+ const error = await response.text();
271
+ throw new Error(`Requesty API error: ${error}`);
272
+ }
273
+ return response.json();
274
+ }
275
+ convertAnthropicToOpenAI(anthropicReq) {
276
+ logger.info('=== STARTING ANTHROPIC TO OPENAI CONVERSION ===');
277
+ const messages = [];
278
+ // Get model-specific tool instructions
279
+ const modelId = anthropicReq.model || this.defaultModel;
280
+ const provider = this.extractProvider(modelId);
281
+ logger.info('Model detection:', {
282
+ requestedModel: anthropicReq.model,
283
+ defaultModel: this.defaultModel,
284
+ finalModelId: modelId,
285
+ extractedProvider: provider
286
+ });
287
+ // CRITICAL: Requesty models use native OpenAI tool calling
288
+ // - If MCP tools are provided, Requesty handles them via function calling
289
+ // - Do NOT inject XML instructions - they cause malformed output
290
+ // - Let Requesty models use tools via OpenAI's tool_calls format
291
+ let systemContent = '';
292
+ // Check if we have MCP tools (function calling)
293
+ const hasMcpTools = anthropicReq.tools && anthropicReq.tools.length > 0;
294
+ logger.info('Tool detection:', {
295
+ hasMcpTools,
296
+ toolCount: anthropicReq.tools?.length || 0,
297
+ toolNames: anthropicReq.tools?.map(t => t.name) || []
298
+ });
299
+ if (hasMcpTools) {
300
+ // MCP tools present - Requesty will handle via function calling
301
+ systemContent = 'You are a helpful AI assistant. When you need to perform actions, use the available tools by calling functions. Always explain what you\'re doing.';
302
+ logger.info('Using MCP tools system prompt (with function calling support)');
303
+ }
304
+ else {
305
+ // No tools - simple response mode
306
+ systemContent = 'You are a helpful AI assistant. Provide clear, well-formatted code and explanations.';
307
+ logger.info('Using simple system prompt (no tools)');
308
+ }
309
+ if (anthropicReq.system) {
310
+ // System can be string OR array of content blocks
311
+ let originalSystem;
312
+ if (typeof anthropicReq.system === 'string') {
313
+ originalSystem = anthropicReq.system;
314
+ }
315
+ else if (Array.isArray(anthropicReq.system)) {
316
+ // Extract text from content blocks
317
+ originalSystem = anthropicReq.system
318
+ .filter(block => block.type === 'text' && block.text)
319
+ .map(block => block.text)
320
+ .join('\n');
321
+ }
322
+ else {
323
+ originalSystem = '';
324
+ }
325
+ logger.info('Appending original system prompt:', {
326
+ systemType: typeof anthropicReq.system,
327
+ isArray: Array.isArray(anthropicReq.system),
328
+ originalSystemLength: originalSystem.length,
329
+ originalSystemPreview: originalSystem.substring(0, 200)
330
+ });
331
+ if (originalSystem) {
332
+ systemContent += '\n\n' + originalSystem;
333
+ }
334
+ }
335
+ messages.push({
336
+ role: 'system',
337
+ content: systemContent
338
+ });
339
+ logger.info('System message created:', {
340
+ systemContentLength: systemContent.length,
341
+ systemContentPreview: systemContent.substring(0, 300)
342
+ });
343
+ // Override model - if request has a Claude model, use defaultModel instead
344
+ const requestedModel = anthropicReq.model || '';
345
+ const shouldOverrideModel = requestedModel.startsWith('claude-') || !requestedModel;
346
+ const finalModel = shouldOverrideModel ? this.defaultModel : requestedModel;
347
+ // Convert Anthropic messages to OpenAI format
348
+ for (const msg of anthropicReq.messages) {
349
+ let content;
350
+ if (typeof msg.content === 'string') {
351
+ content = msg.content;
352
+ }
353
+ else if (Array.isArray(msg.content)) {
354
+ // Extract text from content blocks
355
+ content = msg.content
356
+ .filter(block => block.type === 'text')
357
+ .map(block => block.text)
358
+ .join('\n');
359
+ }
360
+ else {
361
+ content = '';
362
+ }
363
+ messages.push({
364
+ role: msg.role,
365
+ content
366
+ });
367
+ }
368
+ // Get appropriate max_tokens for this model
369
+ let maxTokens = getMaxTokensForModel(finalModel, anthropicReq.max_tokens);
370
+ // Cap at 8192 for OpenAI models via Requesty
371
+ if (maxTokens && maxTokens > 8192) {
372
+ maxTokens = 8192;
373
+ }
374
+ const openaiReq = {
375
+ model: finalModel,
376
+ messages,
377
+ max_tokens: maxTokens,
378
+ temperature: anthropicReq.temperature,
379
+ stream: anthropicReq.stream
380
+ };
381
+ // Convert MCP/Anthropic tools to OpenAI tools format
382
+ if (anthropicReq.tools && anthropicReq.tools.length > 0) {
383
+ logger.info('Converting MCP tools to OpenAI format...');
384
+ openaiReq.tools = anthropicReq.tools.map(tool => {
385
+ const openaiTool = {
386
+ type: 'function',
387
+ function: {
388
+ name: tool.name,
389
+ description: tool.description || '',
390
+ parameters: tool.input_schema || {
391
+ type: 'object',
392
+ properties: {},
393
+ required: []
394
+ }
395
+ }
396
+ };
397
+ logger.info(`Converted tool: ${tool.name}`, {
398
+ hasDescription: !!tool.description,
399
+ hasInputSchema: !!tool.input_schema
400
+ });
401
+ return openaiTool;
402
+ });
403
+ logger.info('Forwarding MCP tools to Requesty', {
404
+ toolCount: openaiReq.tools.length,
405
+ toolNames: openaiReq.tools.map(t => t.function.name)
406
+ });
407
+ }
408
+ else {
409
+ logger.info('No MCP tools to convert');
410
+ }
411
+ logger.info('=== CONVERSION COMPLETE ===', {
412
+ messageCount: openaiReq.messages.length,
413
+ hasMcpTools: !!openaiReq.tools,
414
+ toolCount: openaiReq.tools?.length || 0,
415
+ maxTokens: openaiReq.max_tokens,
416
+ model: openaiReq.model
417
+ });
418
+ return openaiReq;
419
+ }
420
+ parseStructuredCommands(text) {
421
+ const toolUses = [];
422
+ let cleanText = text;
423
+ // Parse file_write commands
424
+ const fileWriteRegex = /<file_write path="([^"]+)">([\s\S]*?)<\/file_write>/g;
425
+ let match;
426
+ while ((match = fileWriteRegex.exec(text)) !== null) {
427
+ toolUses.push({
428
+ type: 'tool_use',
429
+ id: `tool_${Date.now()}_${toolUses.length}`,
430
+ name: 'Write',
431
+ input: {
432
+ file_path: match[1],
433
+ content: match[2].trim()
434
+ }
435
+ });
436
+ cleanText = cleanText.replace(match[0], `[File written: ${match[1]}]`);
437
+ }
438
+ // Parse file_read commands
439
+ const fileReadRegex = /<file_read path="([^"]+)"\/>/g;
440
+ while ((match = fileReadRegex.exec(text)) !== null) {
441
+ toolUses.push({
442
+ type: 'tool_use',
443
+ id: `tool_${Date.now()}_${toolUses.length}`,
444
+ name: 'Read',
445
+ input: {
446
+ file_path: match[1]
447
+ }
448
+ });
449
+ cleanText = cleanText.replace(match[0], `[Reading file: ${match[1]}]`);
450
+ }
451
+ // Parse bash commands
452
+ const bashRegex = /<bash_command>([\s\S]*?)<\/bash_command>/g;
453
+ while ((match = bashRegex.exec(text)) !== null) {
454
+ toolUses.push({
455
+ type: 'tool_use',
456
+ id: `tool_${Date.now()}_${toolUses.length}`,
457
+ name: 'Bash',
458
+ input: {
459
+ command: match[1].trim()
460
+ }
461
+ });
462
+ cleanText = cleanText.replace(match[0], `[Executing: ${match[1].trim()}]`);
463
+ }
464
+ return { cleanText: cleanText.trim(), toolUses };
465
+ }
466
+ convertOpenAIToAnthropic(openaiRes) {
467
+ const choice = openaiRes.choices?.[0];
468
+ if (!choice) {
469
+ throw new Error('No choices in OpenAI response');
470
+ }
471
+ const message = choice.message || {};
472
+ const rawText = message.content || choice.text || '';
473
+ const toolCalls = message.tool_calls || [];
474
+ logger.info('=== CONVERTING OPENAI TO ANTHROPIC ===', {
475
+ hasMessage: !!message,
476
+ hasContent: !!rawText,
477
+ contentLength: rawText?.length,
478
+ hasToolCalls: toolCalls.length > 0,
479
+ toolCallCount: toolCalls.length,
480
+ finishReason: choice.finish_reason
481
+ });
482
+ // CRITICAL: Use ONLY native OpenAI tool_calls format
483
+ // Do NOT parse XML from text - models output malformed XML
484
+ // Requesty handles tools via OpenAI function calling standard
485
+ const contentBlocks = [];
486
+ // Add tool uses from OpenAI tool_calls (MCP tools via function calling)
487
+ if (toolCalls.length > 0) {
488
+ logger.info('Processing tool calls from OpenAI response...');
489
+ for (const toolCall of toolCalls) {
490
+ try {
491
+ logger.info('Tool call details:', {
492
+ id: toolCall.id,
493
+ name: toolCall.function.name,
494
+ argumentsRaw: toolCall.function.arguments
495
+ });
496
+ contentBlocks.push({
497
+ type: 'tool_use',
498
+ id: toolCall.id,
499
+ name: toolCall.function.name,
500
+ input: JSON.parse(toolCall.function.arguments || '{}')
501
+ });
502
+ }
503
+ catch (error) {
504
+ logger.error('Failed to parse tool call arguments', {
505
+ toolCall,
506
+ error: error.message
507
+ });
508
+ }
509
+ }
510
+ logger.info('Converted Requesty tool calls to Anthropic format', {
511
+ toolCallCount: toolCalls.length,
512
+ toolNames: toolCalls.map((tc) => tc.function.name)
513
+ });
514
+ }
515
+ // Add text response if present
516
+ if (rawText && rawText.trim()) {
517
+ logger.info('Adding text content block', {
518
+ textLength: rawText.length,
519
+ textPreview: rawText.substring(0, 200)
520
+ });
521
+ contentBlocks.push({
522
+ type: 'text',
523
+ text: rawText
524
+ });
525
+ }
526
+ // If no content blocks, add empty text
527
+ if (contentBlocks.length === 0) {
528
+ logger.warn('No content blocks found, adding empty text block');
529
+ contentBlocks.push({
530
+ type: 'text',
531
+ text: rawText || ''
532
+ });
533
+ }
534
+ logger.info('Final content blocks:', {
535
+ blockCount: contentBlocks.length,
536
+ blockTypes: contentBlocks.map(b => b.type)
537
+ });
538
+ const result = {
539
+ id: openaiRes.id || `msg_${Date.now()}`,
540
+ type: 'message',
541
+ role: 'assistant',
542
+ model: openaiRes.model,
543
+ content: contentBlocks,
544
+ stop_reason: this.mapFinishReason(choice.finish_reason),
545
+ usage: {
546
+ input_tokens: openaiRes.usage?.prompt_tokens || 0,
547
+ output_tokens: openaiRes.usage?.completion_tokens || 0
548
+ }
549
+ };
550
+ logger.info('Conversion complete, returning Anthropic response');
551
+ return result;
552
+ }
553
+ convertOpenAIStreamToAnthropic(chunk) {
554
+ // Convert OpenAI SSE format to Anthropic SSE format
555
+ const lines = chunk.split('\n').filter(line => line.trim());
556
+ const anthropicChunks = [];
557
+ for (const line of lines) {
558
+ if (line.startsWith('data: ')) {
559
+ const data = line.slice(6);
560
+ if (data === '[DONE]') {
561
+ anthropicChunks.push('event: message_stop\ndata: {}\n\n');
562
+ continue;
563
+ }
564
+ try {
565
+ const parsed = JSON.parse(data);
566
+ const delta = parsed.choices?.[0]?.delta;
567
+ if (delta?.content) {
568
+ anthropicChunks.push(`event: content_block_delta\ndata: ${JSON.stringify({
569
+ type: 'content_block_delta',
570
+ delta: { type: 'text_delta', text: delta.content }
571
+ })}\n\n`);
572
+ }
573
+ }
574
+ catch (e) {
575
+ // Ignore parse errors
576
+ }
577
+ }
578
+ }
579
+ return anthropicChunks.join('');
580
+ }
581
+ extractProvider(modelId) {
582
+ // Extract provider from model ID (e.g., "openai/gpt-4" -> "openai")
583
+ const parts = modelId.split('/');
584
+ return parts.length > 1 ? parts[0] : '';
585
+ }
586
+ mapFinishReason(reason) {
587
+ const mapping = {
588
+ 'stop': 'end_turn',
589
+ 'length': 'max_tokens',
590
+ 'content_filter': 'stop_sequence',
591
+ 'function_call': 'tool_use'
592
+ };
593
+ return mapping[reason || 'stop'] || 'end_turn';
594
+ }
595
+ start(port) {
596
+ this.app.listen(port, () => {
597
+ logger.info('Anthropic to Requesty proxy started', {
598
+ port,
599
+ requestyBaseUrl: this.requestyBaseUrl,
600
+ defaultModel: this.defaultModel
601
+ });
602
+ console.log(`\n✅ Anthropic Proxy running at http://localhost:${port}`);
603
+ console.log(` Requesty Base URL: ${this.requestyBaseUrl}`);
604
+ console.log(` Default Model: ${this.defaultModel}`);
605
+ if (this.capabilities?.requiresEmulation) {
606
+ console.log(`\n ⚙️ Tool Emulation: ${this.capabilities.emulationStrategy.toUpperCase()} pattern`);
607
+ console.log(` 📊 Expected reliability: ${this.capabilities.emulationStrategy === 'react' ? '70-85%' : '50-70%'}`);
608
+ }
609
+ console.log('');
610
+ });
611
+ }
612
+ }
613
+ // CLI entry point
614
+ if (import.meta.url === `file://${process.argv[1]}`) {
615
+ const port = parseInt(process.env.PORT || '3000');
616
+ const requestyApiKey = process.env.REQUESTY_API_KEY;
617
+ if (!requestyApiKey) {
618
+ console.error('❌ Error: REQUESTY_API_KEY environment variable required');
619
+ process.exit(1);
620
+ }
621
+ const proxy = new AnthropicToRequestyProxy({
622
+ requestyApiKey,
623
+ requestyBaseUrl: process.env.ANTHROPIC_PROXY_BASE_URL,
624
+ defaultModel: process.env.COMPLETION_MODEL || process.env.REASONING_MODEL
625
+ });
626
+ proxy.start(port);
627
+ }
@@ -43,6 +43,28 @@ const MODEL_CAPABILITIES = {
43
43
  emulationStrategy: 'none',
44
44
  costPerMillionTokens: 0.30
45
45
  },
46
+ // OpenAI Models (via OpenRouter/Requesty)
47
+ 'openai/gpt-4o': {
48
+ supportsNativeTools: true,
49
+ contextWindow: 128000,
50
+ requiresEmulation: false,
51
+ emulationStrategy: 'none',
52
+ costPerMillionTokens: 2.50
53
+ },
54
+ 'openai/gpt-4o-mini': {
55
+ supportsNativeTools: true,
56
+ contextWindow: 128000,
57
+ requiresEmulation: false,
58
+ emulationStrategy: 'none',
59
+ costPerMillionTokens: 0.15
60
+ },
61
+ 'openai/gpt-4-turbo': {
62
+ supportsNativeTools: true,
63
+ contextWindow: 128000,
64
+ requiresEmulation: false,
65
+ emulationStrategy: 'none',
66
+ costPerMillionTokens: 10.00
67
+ },
46
68
  // OpenRouter - No Native Tool Support (Require Emulation)
47
69
  'mistralai/mistral-7b-instruct': {
48
70
  supportsNativeTools: false,