// proxy.js - KOMPLETT const express = require('express'); const app = express(); const colors = { reset: '\x1b[0m', cyan: '\x1b[36m', green: '\x1b[32m', magenta: '\x1b[35m', yellow: '\x1b[33m', blue: '\x1b[34m', red: '\x1b[31m' }; app.use(express.json({ limit: '50mb' })); function sanitizeToolSchema(schema) { if (!schema || typeof schema !== 'object') { return { type: 'object', properties: {} }; } const clean = JSON.parse(JSON.stringify(schema)); if (!clean.type) clean.type = 'object'; if (!clean.properties) clean.properties = {}; return clean; } function convertAnthropicTools(anthropicTools) { if (!anthropicTools || anthropicTools.length === 0) return []; const validTools = []; for (const tool of anthropicTools) { try { const ollamaTool = { type: 'function', function: { name: tool.name, description: (tool.description || '').substring(0, 500), parameters: sanitizeToolSchema(tool.input_schema) } }; JSON.stringify(ollamaTool); validTools.push(ollamaTool); } catch (e) {} } console.log(`${colors.yellow}Tools: ${validTools.length}${colors.reset}`); return validTools; } function convertAnthropicToOllama(anthropicBody) { const ollamaMessages = []; if (anthropicBody.system) { ollamaMessages.push({ role: 'system', content: typeof anthropicBody.system === 'string' ? anthropicBody.system : JSON.stringify(anthropicBody.system) }); } for (const msg of anthropicBody.messages) { if (typeof msg.content === 'string') { ollamaMessages.push({ role: msg.role, content: msg.content }); } else if (Array.isArray(msg.content)) { const parts = []; for (const item of msg.content) { if (item.type === 'text') { parts.push(item.text); } else if (item.type === 'tool_result') { const resultText = Array.isArray(item.content) ? item.content.map(c => c.text || JSON.stringify(c)).join('\n') : typeof item.content === 'string' ? item.content : JSON.stringify(item.content); console.log(`${colors.blue}πŸ“₯ ${item.tool_use_id}:${colors.reset}`); console.log(`${colors.blue}${resultText}${colors.reset}`); console.log(''); parts.push(`Tool Result (${item.tool_use_id}):\n${resultText}`); } } if (parts.length > 0) { ollamaMessages.push({ role: msg.role, content: parts.join('\n\n') }); } } } const ollamaBody = { model: anthropicBody.model, messages: ollamaMessages, stream: anthropicBody.stream !== false, think: false, // AUF TOP-LEVEL! options: { temperature: 0.7, num_predict: anthropicBody.max_tokens || 4096, num_ctx: 131072 } }; if (anthropicBody.tools && anthropicBody.tools.length > 0) { const validTools = convertAnthropicTools(anthropicBody.tools); if (validTools.length > 0) { ollamaBody.tools = validTools; } } return ollamaBody; } async function handleResponse(response, anthropicBody, res, requestNum) { res.setHeader('Content-Type', 'text/event-stream'); const messageId = 'msg_' + requestNum; res.write(`event: message_start\ndata: ${JSON.stringify({ type: 'message_start', message: { id: messageId, type: 'message', role: 'assistant', content: [], model: anthropicBody.model } })}\n\n`); const reader = response.body.getReader(); const decoder = new TextDecoder(); let contentBlocks = []; let currentBlockIndex = 0; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n').filter(line => line.trim()); for (const line of lines) { try { const data = JSON.parse(line); console.log(`${colors.cyan}[Ollama Response] ${JSON.stringify(data).substring(0, 200)}${colors.reset}`); if (data.message?.tool_calls && data.message.tool_calls.length > 0) { for (const tc of data.message.tool_calls) { console.log(`${colors.yellow}[Raw Tool Call] ${JSON.stringify(tc)}${colors.reset}`); let toolInput = {}; if (typeof tc.function.arguments === 'string') { try { toolInput = JSON.parse(tc.function.arguments); } catch (e) { console.error(`${colors.red}Parse error: ${e.message}${colors.reset}`); } } else if (typeof tc.function.arguments === 'object') { toolInput = tc.function.arguments; } const toolUseId = `toolu_${requestNum}_${currentBlockIndex}`; console.log(`${colors.magenta}[Sending Tool Use: ${tc.function.name}]${colors.reset}`); console.log(`${colors.magenta}Input: ${JSON.stringify(toolInput)}${colors.reset}`); // 1. content_block_start mit LEEREM Input res.write(`event: content_block_start\ndata: ${JSON.stringify({ type: 'content_block_start', index: currentBlockIndex, content_block: { type: 'tool_use', id: toolUseId, name: tc.function.name, input: {} // LEER - wird ΓΌber Delta gesendet! } })}\n\n`); // 2. Input als Delta senden const inputJson = JSON.stringify(toolInput); res.write(`event: content_block_delta\ndata: ${JSON.stringify({ type: 'content_block_delta', index: currentBlockIndex, delta: { type: 'input_json_delta', partial_json: inputJson } })}\n\n`); // 3. content_block_stop res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: 'content_block_stop', index: currentBlockIndex })}\n\n`); currentBlockIndex++; } } if (data.message?.content) { const text = data.message.content; if (contentBlocks[currentBlockIndex] === undefined) { res.write(`event: content_block_start\ndata: ${JSON.stringify({ type: 'content_block_start', index: currentBlockIndex, content_block: { type: 'text', text: '' } })}\n\n`); contentBlocks[currentBlockIndex] = ''; } process.stdout.write(`${colors.green}${text}${colors.reset}`); res.write(`event: content_block_delta\ndata: ${JSON.stringify({ type: 'content_block_delta', index: currentBlockIndex, delta: { type: 'text_delta', text: text } })}\n\n`); contentBlocks[currentBlockIndex] += text; } if (data.done) { if (contentBlocks[currentBlockIndex] !== undefined) { res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: 'content_block_stop', index: currentBlockIndex })}\n\n`); } res.write(`event: message_delta\ndata: ${JSON.stringify({ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: data.eval_count || 0 } })}\n\n`); res.write(`event: message_stop\ndata: ${JSON.stringify({ type: 'message_stop' })}\n\n`); console.log(`${colors.green}βœ“${colors.reset}\n`); } } catch (e) { console.error(`${colors.red}Error: ${e.message}${colors.reset}`); } } } res.end(); } app.post('/v1/messages', async (req, res) => { const requestNum = Date.now(); console.log(`${colors.magenta}━━━ #${requestNum} ━━━${colors.reset}`); try { const anthropicBody = req.body; if (anthropicBody.model?.startsWith('claude-')) { anthropicBody.model = 'qwen3.6:35b-a3b-q4_K_M'; } const ollamaBody = convertAnthropicToOllama(anthropicBody); console.log(`${colors.magenta}[msgs=${ollamaBody.messages.length}, tools=${ollamaBody.tools?.length || 0}, ctx=128k, think=false]${colors.reset}`); const response = await fetch('https://ollama.aquantico.de/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer 324GF44-50AA-4B57-9386-K435DLJ764DFR' }, body: JSON.stringify(ollamaBody) }); if (!response.ok) { const errorText = await response.text(); console.error(`${colors.red}${errorText}${colors.reset}`); throw new Error(`Ollama: ${response.status}`); } return handleResponse(response, anthropicBody, res, requestNum); } catch (error) { console.error(`${colors.red}${error.message}${colors.reset}`); res.status(500).json({ type: 'error', error: { type: 'api_error', message: error.message } }); } }); app.listen(11435, () => { console.log(`${colors.magenta}Proxy: localhost:11435 (128k ctx, think=false)${colors.reset}\n`); });