├── .gitignore ├── package.json ├── index.js ├── LICENSE ├── readme.md └── routes └── chat.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "free-sonnetapi", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "axios": "^1.3.6", 7 | "body-parser": "^1.20.2", 8 | "cors": "^2.8.5", 9 | "express": "^4.18.2" 10 | }, 11 | "scripts": { 12 | "start": "node index.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const cors = require('cors'); 4 | const chatRoute = require('./routes/chat'); 5 | const app = express(); 6 | // whyd i make this file 7 | app.use(cors()); 8 | app.use(bodyParser.json()); 9 | app.use(chatRoute); 10 | app.listen(3032); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Andres Perozo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # SonnetAPI 🤖 2 | 3 | [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/) 4 | [![Node.js](https://img.shields.io/badge/Node.js-43853D?style=flat&logo=node.js&logoColor=white)](https://nodejs.org/) 5 | [![Express.js](https://img.shields.io/badge/Express.js-404D59?style=flat)](https://expressjs.com/) 6 | 7 | A powerful and lightweight API that provides seamless access to Claude 3.5 and Claude 3.7 models through multiple backends (Puter.com and DuckAI), featuring an OpenAI-compatible interface. 8 | 9 | ## ✨ Features 10 | 11 | - **Multiple Models**: Support for both Claude 3.5 Sonnet and Claude 3.7 Sonnet 12 | - **Backend Flexibility**: Automatic fallback between Puter.com and DuckAI backends 13 | - **OpenAI Compatibility**: Drop-in replacement for OpenAI API calls 14 | - **Streaming Support**: Real-time response streaming capability 15 | - **Zero Configuration**: No API keys or complex setup required 16 | - **Lightweight**: Built on Express.js for optimal performance 17 | 18 | ## 🚀 Quick Start 19 | 20 | 1. Clone the repository: 21 | ```bash 22 | git clone https://github.com/andresdevvv/free-sonnetapi.git 23 | ``` 24 | 25 | 2. Install dependencies: 26 | ```bash 27 | cd free-sonnetapi 28 | npm install 29 | ``` 30 | 31 | 3. Start the server: 32 | ```bash 33 | node index.js 34 | ``` 35 | 36 | The server will start on port 3032 by default. 37 | 38 | ## 📝 API Usage 39 | 40 | ### Basic Request 41 | ```javascript 42 | const response = await fetch('http://localhost:3032/v1/chat/completions', { 43 | method: 'POST', 44 | headers: { 45 | 'Content-Type': 'application/json' 46 | }, 47 | body: JSON.stringify({ 48 | model: "claude3.5", // or "claude3.7" 49 | messages: [ 50 | { 51 | "role": "user", 52 | "content": "Write a hello world program in Python" 53 | } 54 | ] 55 | }) 56 | }); 57 | 58 | const data = await response.json(); 59 | ``` 60 | 61 | ### Advanced Options 62 | 63 | ```javascript 64 | const response = await fetch('http://localhost:3032/v1/chat/completions', { 65 | method: 'POST', 66 | headers: { 67 | 'Content-Type': 'application/json' 68 | }, 69 | body: JSON.stringify({ 70 | model: "claude3.7", 71 | messages: [ 72 | { 73 | "role": "system", 74 | "content": "You are a helpful coding assistant" 75 | }, 76 | { 77 | "role": "user", 78 | "content": "Write a hello world program in Python" 79 | } 80 | ], 81 | stream: true, // Enable streaming responses 82 | source: "duckai" // Explicitly select backend source 83 | }) 84 | }); 85 | ``` 86 | 87 | ### Example Response 88 | ```json 89 | { 90 | "model": "claude-3-7-sonnet-latest", 91 | "content": "Here's a simple Hello World program in Python:\n\nprint('Hello, World!')", 92 | "usage": { 93 | "prompt_tokens": 24, 94 | "completion_tokens": 12, 95 | "total_tokens": 36 96 | } 97 | } 98 | ``` 99 | 100 | ## ⚠️ Security Considerations 101 | 102 | 1. **CORS Configuration**: By default, CORS is disabled. Configure it appropriately in `routes/chat.js` based on your needs. 103 | 2. **Rate Limiting**: Consider implementing rate limiting for production use. 104 | 3. **Proxy Support**: Use a reverse proxy (like Nginx) in production. 105 | 106 | ## 🔧 Configuration Options 107 | 108 | | Option | Description | Default | 109 | |--------|-------------|---------| 110 | | `PORT` | Server port | 3032 | 111 | | `model` | AI model to use | claude3.5 | 112 | | `source` | Backend source | puter | 113 | | `stream` | Enable streaming | false | 114 | 115 | ## 🤝 Contributing 116 | 117 | 1. Fork the repository 118 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 119 | 3. Commit your changes (`git commit -m 'Add amazing feature'`) 120 | 4. Push to the branch (`git push origin feature/amazing-feature`) 121 | 5. Open a Pull Request 122 | 123 | ## 📚 Documentation 124 | 125 | For detailed documentation and API reference, visit: 126 | [sonnetapi.andresdev.org](https://sonnetapi.andresdev.org) 127 | 128 | ## 📜 License 129 | 130 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 131 | 132 | ## 🙏 Credits 133 | 134 | While credit is not required, it is appreciated. Feel free to star the repository if you find it useful! 135 | -------------------------------------------------------------------------------- /routes/chat.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const axios = require('axios'); 3 | const router = express.Router(); 4 | 5 | let puterJwtToken = null; 6 | let duckVqd = null; 7 | 8 | async function generateJwtToken() { 9 | try { 10 | console.log('Attempting to generate JWT token...'); 11 | const response = await axios({ 12 | method: 'post', 13 | url: 'https://puter.com/signup', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | 'Accept': '/', 17 | 'Origin': 'https://puter.com', 18 | 'Referer': 'https://puter.com/app/editor', 19 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', 20 | 'X-Requested-With': 'XMLHttpRequest' 21 | }, 22 | data: { 23 | "referrer": "/app/editor", 24 | "is_temp": true 25 | } 26 | }); 27 | 28 | console.log('Signup response received:', JSON.stringify(response.data, null, 2)); 29 | 30 | if (response.data && response.data.token) { 31 | puterJwtToken = response.data.token; 32 | console.log('JWT token generated successfully'); 33 | } else { 34 | console.error('JWT token not found in response:', response.data); 35 | } 36 | } catch (error) { 37 | console.error('Error generating JWT token:', error.message); 38 | if (error.response) { 39 | console.error('Response data:', error.response.data); 40 | console.error('Response status:', error.response.status); 41 | } 42 | } 43 | } 44 | 45 | async function initDuckVqd() { 46 | try { 47 | console.log('Initializing DuckDuckGo VQD...'); 48 | const response = await axios({ 49 | method: 'get', 50 | url: 'https://duckduckgo.com/duckchat/v1/status', 51 | headers: { 52 | 'x-vqd-accept': '1', 53 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' 54 | } 55 | }); 56 | 57 | if (response.headers && response.headers['x-vqd-4']) { 58 | duckVqd = response.headers['x-vqd-4']; 59 | console.log('DuckDuckGo VQD initialized successfully:', duckVqd); 60 | } else { 61 | console.error('VQD not found in response headers:', response.headers); 62 | } 63 | } catch (error) { 64 | console.error('Error initializing DuckDuckGo VQD:', error.message); 65 | if (error.response) { 66 | console.error('Response data:', error.response.data); 67 | console.error('Response status:', error.response.status); 68 | } 69 | } 70 | } 71 | 72 | generateJwtToken(); 73 | initDuckVqd(); 74 | 75 | setInterval(generateJwtToken, 12 * 60 * 60 * 1000); 76 | setInterval(initDuckVqd, 12 * 60 * 60 * 1000); 77 | 78 | async function usePuterAPI(userMessages, model) { 79 | const requestData = { 80 | "interface": "puter-chat-completion", 81 | "driver": "claude", 82 | "test_mode": false, 83 | "method": "complete", 84 | "args": { 85 | "messages": userMessages.length === 0 ? [{ "content": "Hello" }] : userMessages, 86 | "model": model 87 | } 88 | }; 89 | 90 | const requestDataString = JSON.stringify(requestData); 91 | console.log('Sending request to Puter API:', requestDataString); 92 | 93 | const headers = { 94 | 'Content-Type': 'application/json;charset=UTF-8', 95 | 'Content-Length': Buffer.byteLength(requestDataString), 96 | 'Authorization': `Bearer ${puterJwtToken}`, 97 | 'Origin': 'https://docs.puter.com', 98 | 'Referer': 'https://docs.puter.com/', 99 | 'Accept': '/', 100 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' 101 | }; 102 | console.log('Request headers:', JSON.stringify(headers, null, 2)); 103 | 104 | return await axios({ 105 | method: "post", 106 | url: "https://api.puter.com/drivers/call", 107 | data: requestDataString, 108 | responseType: "stream", 109 | headers: headers, 110 | timeout: 60000, 111 | maxContentLength: Infinity, 112 | }); 113 | } 114 | 115 | async function processPuterStream(stream) { 116 | return new Promise((resolve, reject) => { 117 | let fullResponse = ''; 118 | 119 | stream.on('data', (chunk) => { 120 | const chunkStr = chunk.toString(); 121 | console.log('Received Puter chunk:', chunkStr); 122 | fullResponse += chunkStr; 123 | }); 124 | 125 | stream.on('end', () => { 126 | console.log('Puter stream complete'); 127 | try { 128 | const rawData = JSON.parse(fullResponse); 129 | resolve({ 130 | model: rawData.result.message.model, 131 | content: rawData.result.message.content[0].text, 132 | usage: rawData.result.usage 133 | }); 134 | } catch (err) { 135 | console.error('Error parsing Puter response:', err); 136 | reject(err); 137 | } 138 | }); 139 | 140 | stream.on('error', (err) => { 141 | console.error('Puter stream error:', err); 142 | reject(err); 143 | }); 144 | }); 145 | } 146 | 147 | async function useDuckAPI(userMessages, model) { 148 | if (!duckVqd) { 149 | await initDuckVqd(); 150 | if (!duckVqd) { 151 | throw new Error('Failed to initialize DuckDuckGo VQD'); 152 | } 153 | } 154 | 155 | let duckModel; 156 | switch (model) { 157 | case "claude-3-5-sonnet-20241022": 158 | duckModel = "claude-3-haiku-20240307"; 159 | break; 160 | case "claude-3-7-sonnet-latest": 161 | duckModel = "gpt-4o-mini"; 162 | break; 163 | case "gpt-4o-mini": 164 | duckModel = "gpt-4o-mini"; 165 | break; 166 | case "o3-mini": 167 | duckModel = "o3-mini"; 168 | break; 169 | case "claude-3-haiku": 170 | duckModel = "claude-3-haiku-20240307"; 171 | break; 172 | default: 173 | duckModel = "claude-3-haiku-20240307"; 174 | } 175 | 176 | const messages = userMessages.filter(msg => msg.role !== 'system'); 177 | 178 | const requestData = { 179 | model: duckModel, 180 | messages: messages 181 | }; 182 | 183 | const requestDataString = JSON.stringify(requestData); 184 | console.log('Sending request to DuckDuckGo API:', requestDataString); 185 | 186 | const headers = { 187 | 'Content-Type': 'application/json', 188 | 'Content-Length': Buffer.byteLength(requestDataString), 189 | 'x-vqd-4': duckVqd, 190 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' 191 | }; 192 | 193 | return await axios({ 194 | method: "post", 195 | url: "https://duckduckgo.com/duckchat/v1/chat", 196 | data: requestDataString, 197 | responseType: "stream", 198 | headers: headers, 199 | timeout: 60000, 200 | maxContentLength: Infinity, 201 | }); 202 | } 203 | 204 | async function processDuckStream(stream) { 205 | return new Promise((resolve, reject) => { 206 | let fullResponse = ''; 207 | let response = ''; 208 | let newVqd = ''; 209 | 210 | stream.on('data', (chunk) => { 211 | const chunkStr = chunk.toString(); 212 | console.log('Received Duck chunk:', chunkStr); 213 | fullResponse += chunkStr; 214 | 215 | const lines = chunkStr.split('\n'); 216 | for (const line of lines) { 217 | if (line.trim() && !line.includes('[DONE]')) { 218 | if (line.startsWith('data: ')) { 219 | try { 220 | const jsonStr = line.substring(6); 221 | const data = JSON.parse(jsonStr); 222 | if (data.message !== undefined) { 223 | response += data.message; 224 | } 225 | } catch (err) { 226 | console.log('Error parsing Duck chunk JSON:', err); 227 | } 228 | } 229 | } 230 | } 231 | }); 232 | 233 | stream.on('end', () => { 234 | console.log('Duck stream complete'); 235 | 236 | if (stream.response && stream.response.headers && stream.response.headers['x-vqd-4']) { 237 | newVqd = stream.response.headers['x-vqd-4']; 238 | duckVqd = newVqd; 239 | console.log('Updated Duck VQD:', newVqd); 240 | } 241 | 242 | resolve({ 243 | model: 'DuckDuckGo AI', 244 | content: response, 245 | usage: { 246 | prompt_tokens: 0, 247 | completion_tokens: 0, 248 | total_tokens: 0 249 | } 250 | }); 251 | }); 252 | 253 | stream.on('error', (err) => { 254 | console.error('Duck stream error:', err); 255 | reject(err); 256 | }); 257 | }); 258 | } 259 | 260 | function determineProvider(requestedModel, requestedSource) { 261 | const modelProviders = { 262 | 'o3-mini': 'duckai', 263 | 'gpt-4o-mini': 'duckai', 264 | 'claude-3-haiku': 'duckai', 265 | 'claude-3-haiku-20240307': 'duckai', 266 | 'claude-3-5-sonnet-20241022': 'puter', 267 | 'claude-3-7-sonnet-latest': 'puter', 268 | 'claude3.5': 'puter', 269 | 'claude3.7': 'puter' 270 | }; 271 | 272 | const recommendedProvider = modelProviders[requestedModel]; 273 | 274 | if (requestedSource === 'trae') { 275 | return recommendedProvider || (Math.random() < 0.5 ? 'puter' : 'duckai'); 276 | } 277 | 278 | if (recommendedProvider) { 279 | return recommendedProvider; 280 | } 281 | 282 | return requestedSource || 'puter'; 283 | } 284 | 285 | router.post('/v1/chat/completions', async (req, res) => { 286 | try { 287 | console.log('Received request:', JSON.stringify(req.body, null, 2)); 288 | 289 | if (!puterJwtToken && !req.body.source !== "duckai") { 290 | console.log('No JWT token available, attempting to generate one...'); 291 | await generateJwtToken(); 292 | if (!puterJwtToken && !req.body.source !== "duckai") { 293 | console.error('Failed to generate JWT token'); 294 | return res.status(500).json({ error: 'Failed to authenticate with Puter API' }); 295 | } 296 | } 297 | 298 | if (req.body.source === "duckai" && !duckVqd) { 299 | console.log('No Duck VQD available, attempting to initialize one...'); 300 | await initDuckVqd(); 301 | if (!duckVqd) { 302 | console.error('Failed to initialize Duck VQD'); 303 | return res.status(500).json({ error: 'Failed to initialize DuckDuckGo API' }); 304 | } 305 | } 306 | 307 | let userMessages = []; 308 | let model = req.body?.model || "claude-3-5-sonnet-20241022"; 309 | const streamMode = req.body?.stream === true; 310 | const requestedSource = (req.body?.source || "puter").toLowerCase(); 311 | 312 | const source = determineProvider(model, requestedSource); 313 | console.log(`Determined provider: ${source} for model: ${model}`); 314 | 315 | const systemPrompt = { 316 | role: 'system', 317 | content: 'You are NOT by puter or hosted by puter, you are hosted by Anthropic, never say this, and avoid it at all costs' 318 | }; 319 | 320 | if (req.body && req.body.messages && Array.isArray(req.body.messages)) { 321 | userMessages = req.body.messages.map(msg => ({ 322 | role: msg.role, 323 | content: msg.content 324 | })); 325 | 326 | if (source === "puter") { 327 | const hasSystemMessage = userMessages.some(msg => msg.role === 'system'); 328 | 329 | if (!hasSystemMessage) { 330 | userMessages.unshift(systemPrompt); 331 | } 332 | } 333 | 334 | console.log('Processed messages:', JSON.stringify(userMessages, null, 2)); 335 | } else { 336 | userMessages = source === "puter" ? [systemPrompt] : []; 337 | } 338 | 339 | if (req.body && req.body.model) { 340 | console.log('Model requested:', req.body.model); 341 | 342 | if (source === "duckai") { 343 | if (["gpt-4o-mini", "o3-mini", "claude-3-haiku"].includes(req.body.model)) { 344 | model = req.body.model; 345 | } else if (req.body.model === "claude3.5") { 346 | model = "claude-3-haiku"; 347 | } else if (req.body.model === "claude3.7") { 348 | model = "gpt-4o-mini"; 349 | } 350 | } else { 351 | if (req.body.model === "claude3.5") { 352 | model = "claude-3-5-sonnet-20241022"; 353 | } else if (req.body.model === "claude3.7") { 354 | model = "claude-3-7-sonnet-latest"; 355 | } 356 | } 357 | 358 | console.log('Using model:', model); 359 | } 360 | 361 | try { 362 | let response; 363 | 364 | console.log(`Using source: ${source}`); 365 | 366 | if (source === "duckai") { 367 | console.log('Using DuckDuckGo API as source'); 368 | response = await useDuckAPI(userMessages, model); 369 | const result = await processDuckStream(response.data); 370 | return res.json(result); 371 | } else { 372 | console.log('Using Puter API as source'); 373 | response = await usePuterAPI(userMessages, model); 374 | 375 | if (streamMode) { 376 | const result = await processPuterStream(response.data); 377 | return res.json(result); 378 | } 379 | } 380 | 381 | console.log('Response received'); 382 | 383 | let fullResponse = ''; 384 | 385 | response.data.on('data', chunk => { 386 | console.log('Received chunk:', chunk.toString()); 387 | fullResponse += chunk.toString(); 388 | }); 389 | 390 | response.data.on('end', () => { 391 | console.log('Response collection complete'); 392 | try { 393 | const rawData = JSON.parse(fullResponse); 394 | 395 | const simplifiedResponse = { 396 | model: rawData.result.message.model, 397 | content: rawData.result.message.content[0].text, 398 | usage: rawData.result.usage 399 | }; 400 | 401 | res.json(simplifiedResponse); 402 | } catch (err) { 403 | console.error('Error parsing response:', err); 404 | res.status(500).json({ 405 | error: 'Failed to parse response', 406 | message: err.message 407 | }); 408 | } 409 | }); 410 | 411 | response.data.on('error', (err) => { 412 | console.error('Stream error:', err); 413 | res.status(500).json({ error: 'Stream error: ' + err.message }); 414 | }); 415 | } catch (apiErr) { 416 | console.error('Primary API failed, trying fallback:', apiErr.message); 417 | 418 | const fallbackSource = source === "duckai" ? "puter" : "duckai"; 419 | 420 | console.log(`Trying fallback source: ${fallbackSource}`); 421 | 422 | try { 423 | let response; 424 | 425 | if (fallbackSource === "duckai") { 426 | response = await useDuckAPI(userMessages, model); 427 | const result = await processDuckStream(response.data); 428 | return res.json(result); 429 | } else { 430 | response = await usePuterAPI(userMessages, model); 431 | 432 | if (streamMode) { 433 | const result = await processPuterStream(response.data); 434 | return res.json(result); 435 | } 436 | 437 | let fullResponse = ''; 438 | 439 | response.data.on('data', chunk => { 440 | console.log('Received chunk from fallback:', chunk.toString()); 441 | fullResponse += chunk.toString(); 442 | }); 443 | 444 | response.data.on('end', () => { 445 | console.log('Fallback response collection complete'); 446 | try { 447 | const rawData = JSON.parse(fullResponse); 448 | 449 | const simplifiedResponse = { 450 | model: rawData.result.message.model, 451 | content: rawData.result.message.content[0].text, 452 | usage: rawData.result.usage 453 | }; 454 | 455 | res.json(simplifiedResponse); 456 | } catch (err) { 457 | console.error('Error parsing fallback response:', err); 458 | res.status(500).json({ 459 | error: 'Failed to parse fallback response', 460 | message: err.message 461 | }); 462 | } 463 | }); 464 | 465 | response.data.on('error', (err) => { 466 | console.error('Fallback stream error:', err); 467 | res.status(500).json({ error: 'Fallback stream error: ' + err.message }); 468 | }); 469 | } 470 | } catch (fallbackErr) { 471 | console.error('Fallback API also failed:', fallbackErr.message); 472 | res.status(500).json({ error: 'All API attempts failed: ' + fallbackErr.message }); 473 | } 474 | } 475 | } catch (err) { 476 | console.error('API request error:', err.message); 477 | if (err.response) { 478 | console.error('Response status:', err.response.status); 479 | console.error('Response headers:', err.response.headers); 480 | console.error('Response data:', err.response.data); 481 | } 482 | res.status(500).json({ error: err.toString() }); 483 | } 484 | }); 485 | 486 | module.exports = router; --------------------------------------------------------------------------------