605 lines
22 KiB
JavaScript
605 lines
22 KiB
JavaScript
const express = require('express');
|
|
const TurndownService = require('turndown');
|
|
const { parse: parseHtml } = require('node-html-parser');
|
|
const app = express();
|
|
|
|
const OLLAMA_URL = process.env.OLLAMA_URL || 'https://ollama.aquantico.de/api/chat';
|
|
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'qwen3.6:35b-a3b-q4_K_M';
|
|
const OLLAMA_AUTH = process.env.OLLAMA_AUTH || '324GF44-50AA-4B57-9386-K435DLJ764DFR';
|
|
const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY || 'AIzaSyChzsz8ZN8iHRqMUVFnxJXwyXWP_XwWy6g';
|
|
const GOOGLE_CX = process.env.GOOGLE_CX || '2331819c76d874bcc';
|
|
const PORT = parseInt(process.env.PORT || '11435', 10);
|
|
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
cyan: '\x1b[36m',
|
|
green: '\x1b[32m',
|
|
magenta: '\x1b[35m',
|
|
yellow: '\x1b[33m',
|
|
blue: '\x1b[34m',
|
|
red: '\x1b[31m'
|
|
};
|
|
|
|
app.set('trust proxy', 1);
|
|
app.use(express.json({ limit: '50mb' }));
|
|
|
|
// ── Info-Seite ────────────────────────────────────────────────────────────────
|
|
|
|
app.get('/', (req, res) => {
|
|
const host = `${req.protocol}://${req.get('host')}`;
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
res.send(`<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>noThinkProxy</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:system-ui,sans-serif;background:#0f0f0f;color:#e0e0e0;padding:2rem}
|
|
h1{color:#a78bfa;font-size:1.8rem;margin-bottom:.4rem}
|
|
h2{color:#7dd3fc;font-size:1.1rem;margin:2rem 0 .6rem}
|
|
p{color:#9ca3af;line-height:1.6;margin-bottom:.8rem}
|
|
code{background:#1e1e2e;color:#cba6f7;padding:.15rem .4rem;border-radius:.25rem;font-size:.9rem}
|
|
pre{background:#1e1e2e;border:1px solid #333;border-radius:.5rem;padding:1rem;overflow-x:auto;margin:.5rem 0 1rem}
|
|
pre code{background:none;padding:0;color:#a6e3a1}
|
|
.badge{display:inline-block;background:#1e3a5f;color:#7dd3fc;border-radius:.25rem;padding:.1rem .5rem;font-size:.8rem;margin-left:.5rem}
|
|
table{width:100%;border-collapse:collapse;margin:.5rem 0 1rem}
|
|
td,th{border:1px solid #333;padding:.4rem .8rem;text-align:left}
|
|
th{background:#1e1e2e;color:#7dd3fc}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>noThinkProxy <span class="badge">v1.0</span></h1>
|
|
<p>Anthropic-API → Ollama-Proxy · Think-Modus deaktiviert · Web-Suche aktiv</p>
|
|
|
|
<h2>Aktuelle Konfiguration</h2>
|
|
<table>
|
|
<tr><th>Parameter</th><th>Wert</th></tr>
|
|
<tr><td>Ollama URL</td><td><code>${OLLAMA_URL.replace(/\/api\/chat$/, '')}</code></td></tr>
|
|
<tr><td>Modell</td><td><code>${OLLAMA_MODEL}</code></td></tr>
|
|
<tr><td>Kontext</td><td><code>262144 Token (256k)</code></td></tr>
|
|
<tr><td>Think</td><td><code>false</code></td></tr>
|
|
<tr><td>Web-Suche</td><td><code>Google Custom Search</code></td></tr>
|
|
<tr><td>Proxy-URL</td><td><code>${host}</code></td></tr>
|
|
</table>
|
|
|
|
<h2>localclaude installieren</h2>
|
|
<p>Installiert das Script <code>localclaude</code> nach <code>/usr/local/bin</code> (oder <code>~/.local/bin</code>):</p>
|
|
<pre><code>curl -fsSL ${host}/install.sh | bash</code></pre>
|
|
|
|
<h2>Starten</h2>
|
|
<pre><code>localclaude</code></pre>
|
|
<p><code>localclaude</code> setzt automatisch <code>ANTHROPIC_BASE_URL=${host}</code> und ruft <code>claude</code> auf.</p>
|
|
|
|
<h2>API-Endpunkt</h2>
|
|
<pre><code>POST ${host}/v1/messages</code></pre>
|
|
<p>Alle <code>claude-*</code> Modellnamen werden auf <code>${OLLAMA_MODEL}</code> umgeleitet.</p>
|
|
</body>
|
|
</html>`);
|
|
});
|
|
|
|
// ── Install-Script ────────────────────────────────────────────────────────────
|
|
|
|
app.get('/install.sh', (req, res) => {
|
|
const host = `${req.protocol}://${req.get('host')}`;
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
res.send(`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
PROXY_URL="${host}"
|
|
INSTALL_DIR="/usr/local/bin"
|
|
NEEDS_PATH_UPDATE=false
|
|
|
|
echo ""
|
|
echo "=== noThinkProxy · localclaude Installer ==="
|
|
echo ""
|
|
|
|
# Zielverzeichnis bestimmen (ohne sudo → ~/.local/bin)
|
|
if [ ! -w "\$INSTALL_DIR" ]; then
|
|
INSTALL_DIR="\$HOME/.local/bin"
|
|
mkdir -p "\$INSTALL_DIR"
|
|
fi
|
|
|
|
# localclaude-Script schreiben
|
|
cat > "\$INSTALL_DIR/localclaude" <<'SCRIPT'
|
|
#!/usr/bin/env bash
|
|
export ANTHROPIC_BASE_URL="${host}"
|
|
exec claude "\$@"
|
|
SCRIPT
|
|
|
|
chmod +x "\$INSTALL_DIR/localclaude"
|
|
|
|
# PATH prüfen und ggf. in Shell-Config eintragen
|
|
if ! echo "\$PATH" | grep -q "\$INSTALL_DIR"; then
|
|
NEEDS_PATH_UPDATE=true
|
|
echo "» Trage \$INSTALL_DIR in ~/.bashrc und ~/.zshrc ein..."
|
|
echo "export PATH=\\"\$INSTALL_DIR:\$PATH\\"" >> "\$HOME/.bashrc"
|
|
echo "export PATH=\\"\$INSTALL_DIR:\$PATH\\"" >> "\$HOME/.zshrc" 2>/dev/null || true
|
|
fi
|
|
|
|
echo "✓ localclaude installiert in \$INSTALL_DIR"
|
|
echo ""
|
|
if [ "\$NEEDS_PATH_UPDATE" = "true" ]; then
|
|
echo "────────────────────────────────────────────"
|
|
echo "Führe diesen Befehl jetzt aus damit localclaude sofort verfügbar ist:"
|
|
echo ""
|
|
echo " export PATH=\\"\$INSTALL_DIR:\$PATH\\""
|
|
echo ""
|
|
echo "In neuen Shell-Sessions ist es automatisch verfügbar."
|
|
echo "────────────────────────────────────────────"
|
|
else
|
|
echo "Starte mit: localclaude"
|
|
fi
|
|
echo ""
|
|
`);
|
|
});
|
|
|
|
// ── Web Search ────────────────────────────────────────────────────────────────
|
|
|
|
const WEB_SEARCH_TOOL = {
|
|
type: 'function',
|
|
function: {
|
|
name: 'web_search',
|
|
description: 'Searches the internet for current information. Use this when you need up-to-date facts, recent news, or information you are not certain about.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
query: { type: 'string', description: 'The search query' }
|
|
},
|
|
required: ['query']
|
|
}
|
|
}
|
|
};
|
|
|
|
const READ_URL_TOOL = {
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_url',
|
|
description: 'Fetches a URL and returns the page content as Markdown. Use this to read articles, documentation, or any web page in detail.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
url: { type: 'string', description: 'The URL to fetch' }
|
|
},
|
|
required: ['url']
|
|
}
|
|
}
|
|
};
|
|
|
|
async function executeReadUrl(url) {
|
|
console.log(`${colors.cyan}[Read URL] ${url}${colors.reset}`);
|
|
try {
|
|
const resp = await fetch(url, {
|
|
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; noThinkProxy/1.0)' },
|
|
signal: AbortSignal.timeout(15000)
|
|
});
|
|
if (!resp.ok) return `Error fetching ${url}: HTTP ${resp.status}`;
|
|
|
|
const html = await resp.text();
|
|
const root = parseHtml(html, { blockTextElements: { script: false, style: false } });
|
|
|
|
// Rauschen entfernen
|
|
for (const sel of ['script', 'style', 'nav', 'header', 'footer', 'aside', 'iframe', 'noscript']) {
|
|
root.querySelectorAll(sel).forEach(el => el.remove());
|
|
}
|
|
|
|
const title = root.querySelector('title')?.text.trim()
|
|
|| root.querySelector('h1')?.text.trim()
|
|
|| '';
|
|
|
|
// Hauptinhalt extrahieren
|
|
const mainEl = root.querySelector('main, article, [role="main"], #content, #main, .content, .post');
|
|
const contentHtml = mainEl ? mainEl.innerHTML : (root.querySelector('body')?.innerHTML || html);
|
|
|
|
const td = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
|
|
let markdown = `# ${title}\n\n${td.turndown(contentHtml)}`;
|
|
|
|
if (markdown.length > 12000) {
|
|
markdown = markdown.substring(0, 12000) + '\n\n[... content truncated ...]';
|
|
}
|
|
|
|
console.log(`${colors.cyan}[Read URL] ${markdown.length} Zeichen${colors.reset}`);
|
|
return markdown;
|
|
} catch (e) {
|
|
return `Error fetching ${url}: ${e.message}`;
|
|
}
|
|
}
|
|
|
|
async function executeWebSearch(query) {
|
|
const url = `https://www.googleapis.com/customsearch/v1?key=${GOOGLE_API_KEY}&cx=${GOOGLE_CX}&q=${encodeURIComponent(query)}&num=5`;
|
|
console.log(`${colors.cyan}[Web Search] "${query}"${colors.reset}`);
|
|
try {
|
|
const resp = await fetch(url);
|
|
const data = await resp.json();
|
|
if (data.error) return `Search error: ${data.error.message}`;
|
|
if (!data.items?.length) return `No results found for "${query}".`;
|
|
const results = data.items
|
|
.map((item, i) => `[${i + 1}] ${item.title}\n${item.link}\n${item.snippet}`)
|
|
.join('\n\n');
|
|
console.log(`${colors.cyan}[Web Search] ${data.items.length} Ergebnisse${colors.reset}`);
|
|
return results;
|
|
} catch (e) {
|
|
return `Search error: ${e.message}`;
|
|
}
|
|
}
|
|
|
|
// ── Hilfsfunktionen ───────────────────────────────────────────────────────────
|
|
|
|
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.error(`${colors.red}[Tool Schema Error] ${e.message}${colors.reset}`);
|
|
}
|
|
}
|
|
return validTools;
|
|
}
|
|
|
|
function stringifyToolResultContent(content) {
|
|
if (Array.isArray(content)) {
|
|
return content.map(c => {
|
|
if (typeof c === 'string') return c;
|
|
if (c?.text) return c.text;
|
|
return JSON.stringify(c);
|
|
}).join('\n');
|
|
}
|
|
if (typeof content === 'string') return content;
|
|
return JSON.stringify(content);
|
|
}
|
|
|
|
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 });
|
|
continue;
|
|
}
|
|
if (!Array.isArray(msg.content)) continue;
|
|
|
|
if (msg.role === 'assistant') {
|
|
const textParts = [];
|
|
const toolCalls = [];
|
|
for (const item of msg.content) {
|
|
if (item.type === 'text') textParts.push(item.text || '');
|
|
else if (item.type === 'tool_use') {
|
|
toolCalls.push({ function: { name: item.name, arguments: item.input || {} } });
|
|
}
|
|
}
|
|
const assistantMsg = { role: 'assistant', content: textParts.join('\n\n') };
|
|
if (toolCalls.length > 0) assistantMsg.tool_calls = toolCalls;
|
|
ollamaMessages.push(assistantMsg);
|
|
|
|
} else {
|
|
const pendingText = [];
|
|
for (const item of msg.content) {
|
|
if (item.type === 'text') {
|
|
pendingText.push(item.text || '');
|
|
} else if (item.type === 'tool_result') {
|
|
if (pendingText.length > 0) {
|
|
ollamaMessages.push({ role: 'user', content: pendingText.join('\n\n') });
|
|
pendingText.length = 0;
|
|
}
|
|
const resultText = stringifyToolResultContent(item.content);
|
|
console.log(`${colors.blue}📥 Tool Result ${item.tool_use_id}:${colors.reset}`);
|
|
console.log(`${colors.blue}${resultText}${colors.reset}\n`);
|
|
ollamaMessages.push({ role: 'tool', content: resultText });
|
|
}
|
|
}
|
|
if (pendingText.length > 0) {
|
|
ollamaMessages.push({ role: 'user', content: pendingText.join('\n\n') });
|
|
}
|
|
}
|
|
}
|
|
|
|
const ollamaBody = {
|
|
model: anthropicBody.model,
|
|
messages: ollamaMessages,
|
|
stream: anthropicBody.stream !== false,
|
|
think: false,
|
|
options: {
|
|
temperature: 0.7,
|
|
num_predict: anthropicBody.max_tokens || 4096,
|
|
num_ctx: 262144
|
|
}
|
|
};
|
|
|
|
// Anthropic-Tools konvertieren + interne Tools voranstellen
|
|
const convertedTools = convertAnthropicTools(anthropicBody.tools || []);
|
|
ollamaBody.tools = [WEB_SEARCH_TOOL, READ_URL_TOOL, ...convertedTools];
|
|
|
|
return ollamaBody;
|
|
}
|
|
|
|
function parseToolArguments(args) {
|
|
if (!args) return {};
|
|
if (typeof args === 'string') {
|
|
try { return JSON.parse(args); } catch (e) { return {}; }
|
|
}
|
|
if (typeof args === 'object') return args;
|
|
return {};
|
|
}
|
|
|
|
function makeToolDedupeKey(tc) {
|
|
const name = tc.function?.name || '';
|
|
const args = tc.function?.arguments || {};
|
|
return `${name}:${typeof args === 'string' ? args : JSON.stringify(args)}`;
|
|
}
|
|
|
|
// ── Response-Handler mit Web-Search-Loop ──────────────────────────────────────
|
|
|
|
async function handleResponse(initialResponse, anthropicBody, res, requestNum, ollamaBody) {
|
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|
res.setHeader('Connection', 'keep-alive');
|
|
|
|
res.write(`event: message_start\ndata: ${JSON.stringify({
|
|
type: 'message_start',
|
|
message: {
|
|
id: 'msg_' + requestNum,
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [],
|
|
model: anthropicBody.model,
|
|
stop_reason: null,
|
|
stop_sequence: null,
|
|
usage: { input_tokens: 0, output_tokens: 0 }
|
|
}
|
|
})}\n\n`);
|
|
|
|
let currentBlockIndex = 0;
|
|
let emittedToolUse = false;
|
|
const seenToolCalls = new Set();
|
|
let currentResponse = initialResponse;
|
|
|
|
for (let searchIteration = 0; searchIteration < 5; searchIteration++) {
|
|
const reader = currentResponse.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
let textBlockOpen = false;
|
|
let evalCount = 0;
|
|
|
|
// Gesammelt pro Iteration für eventuelle Web-Search-Fortsetzung
|
|
let iterContent = '';
|
|
let iterAllToolCalls = [];
|
|
let iterWebSearches = [];
|
|
|
|
function processChunk(data) {
|
|
// Tool Calls
|
|
if (data.message?.tool_calls?.length) {
|
|
for (const tc of data.message.tool_calls) {
|
|
iterAllToolCalls.push(tc);
|
|
|
|
// Proxy-eigene Tools + Claude Code Web-Tools intern abfangen
|
|
const INTERNAL = new Set(['web_search', 'read_url', 'WebSearch', 'web_fetch', 'WebFetch']);
|
|
if (INTERNAL.has(tc.function?.name)) {
|
|
iterWebSearches.push(tc);
|
|
continue; // intern verarbeiten, nicht an Client senden
|
|
}
|
|
|
|
const key = makeToolDedupeKey(tc);
|
|
if (seenToolCalls.has(key)) {
|
|
console.log(`${colors.yellow}[Duplicate Tool Call skipped] ${key}${colors.reset}`);
|
|
continue;
|
|
}
|
|
seenToolCalls.add(key);
|
|
emittedToolUse = true;
|
|
|
|
const toolName = tc.function?.name;
|
|
const toolInput = parseToolArguments(tc.function?.arguments);
|
|
const toolUseId = `toolu_${requestNum}_${currentBlockIndex}`;
|
|
|
|
console.log(`${colors.magenta}[Tool Use: ${toolName}] ${JSON.stringify(toolInput)}${colors.reset}`);
|
|
|
|
res.write(`event: content_block_start\ndata: ${JSON.stringify({
|
|
type: 'content_block_start', index: currentBlockIndex,
|
|
content_block: { type: 'tool_use', id: toolUseId, name: toolName, input: {} }
|
|
})}\n\n`);
|
|
res.write(`event: content_block_delta\ndata: ${JSON.stringify({
|
|
type: 'content_block_delta', index: currentBlockIndex,
|
|
delta: { type: 'input_json_delta', partial_json: JSON.stringify(toolInput) }
|
|
})}\n\n`);
|
|
res.write(`event: content_block_stop\ndata: ${JSON.stringify({
|
|
type: 'content_block_stop', index: currentBlockIndex
|
|
})}\n\n`);
|
|
currentBlockIndex++;
|
|
}
|
|
}
|
|
|
|
// Text
|
|
if (data.message?.content) {
|
|
const text = data.message.content;
|
|
iterContent += text;
|
|
|
|
if (!textBlockOpen) {
|
|
res.write(`event: content_block_start\ndata: ${JSON.stringify({
|
|
type: 'content_block_start', index: currentBlockIndex,
|
|
content_block: { type: 'text', text: '' }
|
|
})}\n\n`);
|
|
textBlockOpen = true;
|
|
}
|
|
|
|
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 }
|
|
})}\n\n`);
|
|
}
|
|
|
|
// Done
|
|
if (data.done) {
|
|
evalCount = data.eval_count || 0;
|
|
if (textBlockOpen) {
|
|
res.write(`event: content_block_stop\ndata: ${JSON.stringify({
|
|
type: 'content_block_stop', index: currentBlockIndex
|
|
})}\n\n`);
|
|
currentBlockIndex++;
|
|
textBlockOpen = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stream lesen
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop() || '';
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
try { processChunk(JSON.parse(trimmed)); }
|
|
catch (e) { console.error(`${colors.red}[Parse Error] ${e.message}${colors.reset}`); }
|
|
}
|
|
}
|
|
if (buffer.trim()) {
|
|
try { processChunk(JSON.parse(buffer.trim())); }
|
|
catch (e) { console.error(`${colors.red}[Final Buffer Error] ${e.message}${colors.reset}`); }
|
|
}
|
|
|
|
// Offenen Text-Block schließen falls Ollama keinen done:true geschickt hat
|
|
if (textBlockOpen) {
|
|
res.write(`event: content_block_stop\ndata: ${JSON.stringify({
|
|
type: 'content_block_stop', index: currentBlockIndex
|
|
})}\n\n`);
|
|
currentBlockIndex++;
|
|
textBlockOpen = false;
|
|
}
|
|
|
|
// Keine Web-Suchen → finale Events senden
|
|
if (iterWebSearches.length === 0) {
|
|
res.write(`event: message_delta\ndata: ${JSON.stringify({
|
|
type: 'message_delta',
|
|
delta: { stop_reason: emittedToolUse ? 'tool_use' : 'end_turn' },
|
|
usage: { output_tokens: evalCount }
|
|
})}\n\n`);
|
|
res.write(`event: message_stop\ndata: ${JSON.stringify({ type: 'message_stop' })}\n\n`);
|
|
console.log(`${colors.green}✓${colors.reset}\n`);
|
|
break;
|
|
}
|
|
|
|
// Web-Suchen ausführen und Ergebnisse in Messages einbauen
|
|
ollamaBody.messages.push({
|
|
role: 'assistant',
|
|
content: iterContent,
|
|
tool_calls: iterAllToolCalls
|
|
});
|
|
|
|
for (const tc of iterWebSearches) {
|
|
const args = parseToolArguments(tc.function?.arguments);
|
|
let result;
|
|
const name = tc.function?.name;
|
|
if (name === 'web_search' || name === 'WebSearch') {
|
|
result = await executeWebSearch(args?.query || args?.q || '');
|
|
} else if (name === 'read_url' || name === 'web_fetch' || name === 'WebFetch') {
|
|
result = await executeReadUrl(args?.url || '');
|
|
}
|
|
ollamaBody.messages.push({ role: 'tool', content: result ?? `Tool '${name}' returned no result.` });
|
|
}
|
|
|
|
// Neuer Ollama-Request (weiter streamen)
|
|
currentResponse = await fetch(OLLAMA_URL, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${OLLAMA_AUTH}`
|
|
},
|
|
body: JSON.stringify(ollamaBody)
|
|
});
|
|
|
|
if (!currentResponse.ok) {
|
|
const errText = await currentResponse.text();
|
|
throw new Error(`Ollama: ${currentResponse.status} ${errText}`);
|
|
}
|
|
}
|
|
|
|
res.end();
|
|
}
|
|
|
|
// ── Proxy-Endpunkt ────────────────────────────────────────────────────────────
|
|
|
|
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 = OLLAMA_MODEL;
|
|
}
|
|
|
|
const ollamaBody = convertAnthropicToOllama(anthropicBody);
|
|
|
|
console.log(
|
|
`${colors.magenta}[msgs=${ollamaBody.messages.length}, tools=${ollamaBody.tools.length}, ctx=256k, think=false]${colors.reset}`
|
|
);
|
|
|
|
const response = await fetch(OLLAMA_URL, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${OLLAMA_AUTH}`
|
|
},
|
|
body: JSON.stringify(ollamaBody)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Ollama: ${response.status} ${errorText}`);
|
|
}
|
|
|
|
return handleResponse(response, anthropicBody, res, requestNum, ollamaBody);
|
|
|
|
} catch (error) {
|
|
console.error(`${colors.red}${error.message}${colors.reset}`);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({
|
|
type: 'error',
|
|
error: { type: 'api_error', message: error.message }
|
|
});
|
|
} else {
|
|
res.end();
|
|
}
|
|
}
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`${colors.magenta}noThinkProxy: localhost:${PORT}${colors.reset}`);
|
|
console.log(`${colors.cyan} Ollama : ${OLLAMA_URL}${colors.reset}`);
|
|
console.log(`${colors.cyan} Modell : ${OLLAMA_MODEL}${colors.reset}`);
|
|
console.log(`${colors.cyan} Ctx : 256k Think: false${colors.reset}`);
|
|
console.log(`${colors.cyan} Search : Google Custom Search${colors.reset}\n`);
|
|
});
|