2026-05-10 19:13:06 +02:00
const express = require ( 'express' ) ;
const TurndownService = require ( 'turndown' ) ;
const { parse : parseHtml } = require ( 'node-html-parser' ) ;
2026-05-10 10:46:41 +02:00
const app = express ( ) ;
2026-05-10 19:13:06 +02:00
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 ) ;
2026-05-10 11:11:59 +02:00
2026-05-10 10:46:41 +02:00
const colors = {
2026-05-10 11:11:59 +02:00
reset : '\x1b[0m' ,
cyan : '\x1b[36m' ,
green : '\x1b[32m' ,
2026-05-10 10:46:41 +02:00
magenta : '\x1b[35m' ,
2026-05-10 11:11:59 +02:00
yellow : '\x1b[33m' ,
blue : '\x1b[34m' ,
red : '\x1b[31m'
2026-05-10 10:46:41 +02:00
} ;
2026-05-10 11:11:59 +02:00
app . set ( 'trust proxy' , 1 ) ;
2026-05-10 10:46:41 +02:00
app . use ( express . json ( { limit : '50mb' } ) ) ;
2026-05-10 11:11:59 +02:00
// ── 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 < / t i t l e >
< style >
* { box - sizing : border - box ; margin : 0 ; padding : 0 }
body { font - family : system - ui , sans - serif ; background : # 0 f0f0f ; color : # e0e0e0 ; padding : 2 rem }
h1 { color : # a78bfa ; font - size : 1.8 rem ; margin - bottom : . 4 rem }
h2 { color : # 7 dd3fc ; font - size : 1.1 rem ; margin : 2 rem 0 . 6 rem }
p { color : # 9 ca3af ; line - height : 1.6 ; margin - bottom : . 8 rem }
code { background : # 1e1 e2e ; color : # cba6f7 ; padding : . 15 rem . 4 rem ; border - radius : . 25 rem ; font - size : . 9 rem }
pre { background : # 1e1 e2e ; border : 1 px solid # 333 ; border - radius : . 5 rem ; padding : 1 rem ; overflow - x : auto ; margin : . 5 rem 0 1 rem }
pre code { background : none ; padding : 0 ; color : # a6e3a1 }
. badge { display : inline - block ; background : # 1e3 a5f ; color : # 7 dd3fc ; border - radius : . 25 rem ; padding : . 1 rem . 5 rem ; font - size : . 8 rem ; margin - left : . 5 rem }
table { width : 100 % ; border - collapse : collapse ; margin : . 5 rem 0 1 rem }
td , th { border : 1 px solid # 333 ; padding : . 4 rem . 8 rem ; text - align : left }
th { background : # 1e1 e2e ; color : # 7 dd3fc }
< / s t y l e >
< / h e a d >
< body >
< h1 > noThinkProxy < span class = "badge" > v1 . 0 < / s p a n > < / h 1 >
2026-05-10 19:13:06 +02:00
< p > Anthropic - API → Ollama - Proxy · Think - Modus deaktiviert · Web - Suche aktiv < / p >
2026-05-10 11:11:59 +02:00
< h2 > Aktuelle Konfiguration < / h 2 >
< table >
< tr > < th > Parameter < / t h > < t h > W e r t < / t h > < / t r >
< tr > < td > Ollama URL < /td><td><code>${OLLAMA_URL.replace(/ \ / api \ / chat$ / , '' ) } < / c o d e > < / t d > < / t r >
< tr > < td > Modell < / t d > < t d > < c o d e > $ { O L L A M A _ M O D E L } < / c o d e > < / t d > < / t r >
< tr > < td > Kontext < / t d > < t d > < c o d e > 2 6 2 1 4 4 T o k e n ( 2 5 6 k ) < / c o d e > < / t d > < / t r >
< tr > < td > Think < / t d > < t d > < c o d e > f a l s e < / c o d e > < / t d > < / t r >
2026-05-10 19:13:06 +02:00
< tr > < td > Web - Suche < / t d > < t d > < c o d e > G o o g l e C u s t o m S e a r c h < / c o d e > < / t d > < / t r >
2026-05-10 11:11:59 +02:00
< tr > < td > Proxy - URL < / t d > < t d > < c o d e > $ { h o s t } < / c o d e > < / t d > < / t r >
< / t a b l e >
< h2 > localclaude installieren < / h 2 >
< p > Installiert das Script < code > localclaude < / c o d e > n a c h < c o d e > / u s r / l o c a l / b i n < / c o d e > ( o d e r < c o d e > ~ / . l o c a l / b i n < / c o d e > ) : < / p >
< pre > < code > curl - fsSL $ { host } / install . sh | bash < / c o d e > < / p r e >
< h2 > Starten < / h 2 >
< pre > < code > localclaude < / c o d e > < / p r e >
< p > < code > localclaude < / c o d e > s e t z t a u t o m a t i s c h < c o d e > A N T H R O P I C _ B A S E _ U R L = $ { h o s t } < / c o d e > u n d r u f t < c o d e > c l a u d e < / c o d e > a u f . < / p >
< h2 > API - Endpunkt < / h 2 >
< pre > < code > POST $ { host } / v1 / messages < / c o d e > < / p r e >
2026-05-10 19:13:06 +02:00
< p > Alle < code > claude - * < / c o d e > M o d e l l n a m e n w e r d e n a u f < c o d e > $ { O L L A M A _ M O D E L } < / c o d e > u m g e l e i t e t . < / p >
2026-05-10 11:11:59 +02:00
< / b o d y >
< / h t m l > ` ) ;
} ) ;
// ── 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 → ~ / . l o c a l / b i n )
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 > / d e v / n u l l | | t r u e
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 ""
` );
} ) ;
2026-05-10 19:13:06 +02:00
// ── 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' ]
}
}
} ;
2026-05-10 11:11:59 +02:00
2026-05-10 19:13:06 +02:00
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' ]
}
2026-05-10 10:46:41 +02:00
}
2026-05-10 19:13:06 +02:00
} ;
2026-05-10 10:46:41 +02:00
2026-05-10 19:13:06 +02:00
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 ───────────────────────────────────────────────────────────
2026-05-10 10:46:41 +02:00
2026-05-10 19:13:06 +02:00
function sanitizeToolSchema ( schema ) {
if ( ! schema || typeof schema !== 'object' ) return { type : 'object' , properties : { } } ;
const clean = JSON . parse ( JSON . stringify ( schema ) ) ;
2026-05-10 10:46:41 +02:00
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 ) ) {
2026-05-10 19:13:06 +02:00
return content . map ( c => {
if ( typeof c === 'string' ) return c ;
if ( c ? . text ) return c . text ;
return JSON . stringify ( c ) ;
} ) . join ( '\n' ) ;
2026-05-10 10:46:41 +02:00
}
if ( typeof content === 'string' ) return content ;
return JSON . stringify ( content ) ;
}
function convertAnthropicToOllama ( anthropicBody ) {
const ollamaMessages = [ ] ;
if ( anthropicBody . system ) {
ollamaMessages . push ( {
role : 'system' ,
2026-05-10 19:13:06 +02:00
content : typeof anthropicBody . system === 'string'
? anthropicBody . system
: JSON . stringify ( anthropicBody . system )
2026-05-10 10:46:41 +02:00
} ) ;
}
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 ) {
2026-05-10 19:13:06 +02:00
if ( item . type === 'text' ) textParts . push ( item . text || '' ) ;
else if ( item . type === 'tool_use' ) {
toolCalls . push ( { function : { name : item . name , arguments : item . input || { } } } ) ;
2026-05-10 10:46:41 +02:00
}
}
const assistantMsg = { role : 'assistant' , content : textParts . join ( '\n\n' ) } ;
2026-05-10 19:13:06 +02:00
if ( toolCalls . length > 0 ) assistantMsg . tool _calls = toolCalls ;
2026-05-10 10:46:41 +02:00
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 } ` ) ;
2026-05-10 19:13:06 +02:00
console . log ( ` ${ colors . blue } ${ resultText } ${ colors . reset } \n ` ) ;
2026-05-10 10:46:41 +02:00
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
}
} ;
2026-05-10 19:13:06 +02:00
// Anthropic-Tools konvertieren + interne Tools voranstellen
const convertedTools = convertAnthropicTools ( anthropicBody . tools || [ ] ) ;
ollamaBody . tools = [ WEB _SEARCH _TOOL , READ _URL _TOOL , ... convertedTools ] ;
2026-05-10 10:46:41 +02:00
return ollamaBody ;
}
function parseToolArguments ( args ) {
if ( ! args ) return { } ;
if ( typeof args === 'string' ) {
2026-05-10 19:13:06 +02:00
try { return JSON . parse ( args ) ; } catch ( e ) { return { } ; }
2026-05-10 10:46:41 +02:00
}
2026-05-10 11:11:59 +02:00
if ( typeof args === 'object' ) return args ;
2026-05-10 10:46:41 +02:00
return { } ;
}
function makeToolDedupeKey ( tc ) {
const name = tc . function ? . name || '' ;
const args = tc . function ? . arguments || { } ;
2026-05-10 19:13:06 +02:00
return ` ${ name } : ${ typeof args === 'string' ? args : JSON . stringify ( args ) } ` ;
2026-05-10 10:46:41 +02:00
}
2026-05-10 19:13:06 +02:00
// ── Response-Handler mit Web-Search-Loop ──────────────────────────────────────
2026-05-10 11:11:59 +02:00
2026-05-10 19:13:06 +02:00
async function handleResponse ( initialResponse , anthropicBody , res , requestNum , ollamaBody ) {
2026-05-10 10:46:41 +02:00
res . setHeader ( 'Content-Type' , 'text/event-stream' ) ;
res . setHeader ( 'Cache-Control' , 'no-cache' ) ;
res . setHeader ( 'Connection' , 'keep-alive' ) ;
res . write ( ` event: message_start \n data: ${ JSON . stringify ( {
type : 'message_start' ,
message : {
2026-05-10 19:13:06 +02:00
id : 'msg_' + requestNum ,
2026-05-10 10:46:41 +02:00
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 ;
2026-05-10 19:13:06 +02:00
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
}
2026-05-10 10:46:41 +02:00
2026-05-10 19:13:06 +02:00
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 \n data: ${ 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 \n data: ${ 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 \n data: ${ JSON . stringify ( {
type : 'content_block_stop' , index : currentBlockIndex
} ) } \ n \ n ` );
currentBlockIndex ++ ;
2026-05-10 10:46:41 +02:00
}
2026-05-10 19:13:06 +02:00
}
2026-05-10 10:46:41 +02:00
2026-05-10 19:13:06 +02:00
// Text
if ( data . message ? . content ) {
const text = data . message . content ;
iterContent += text ;
if ( ! textBlockOpen ) {
res . write ( ` event: content_block_start \n data: ${ JSON . stringify ( {
type : 'content_block_start' , index : currentBlockIndex ,
content _block : { type : 'text' , text : '' }
} ) } \ n \ n ` );
textBlockOpen = true ;
}
2026-05-10 10:46:41 +02:00
2026-05-10 19:13:06 +02:00
process . stdout . write ( ` ${ colors . green } ${ text } ${ colors . reset } ` ) ;
2026-05-10 10:46:41 +02:00
res . write ( ` event: content_block_delta \n data: ${ JSON . stringify ( {
2026-05-10 19:13:06 +02:00
type : 'content_block_delta' , index : currentBlockIndex ,
delta : { type : 'text_delta' , text }
2026-05-10 10:46:41 +02:00
} ) } \ n \ n ` );
2026-05-10 19:13:06 +02:00
}
2026-05-10 10:46:41 +02:00
2026-05-10 19:13:06 +02:00
// Done
if ( data . done ) {
evalCount = data . eval _count || 0 ;
if ( textBlockOpen ) {
res . write ( ` event: content_block_stop \n data: ${ JSON . stringify ( {
type : 'content_block_stop' , index : currentBlockIndex
} ) } \ n \ n ` );
currentBlockIndex ++ ;
textBlockOpen = false ;
}
2026-05-10 10:46:41 +02:00
}
}
2026-05-10 19:13:06 +02:00
// 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 } ` ) ; }
2026-05-10 10:46:41 +02:00
}
2026-05-10 19:13:06 +02:00
}
if ( buffer . trim ( ) ) {
try { processChunk ( JSON . parse ( buffer . trim ( ) ) ) ; }
catch ( e ) { console . error ( ` ${ colors . red } [Final Buffer Error] ${ e . message } ${ colors . reset } ` ) ; }
}
2026-05-10 10:46:41 +02:00
2026-05-10 19:13:06 +02:00
// Offenen Text-Block schließen falls Ollama keinen done:true geschickt hat
if ( textBlockOpen ) {
res . write ( ` event: content_block_stop \n data: ${ JSON . stringify ( {
type : 'content_block_stop' , index : currentBlockIndex
2026-05-10 10:46:41 +02:00
} ) } \ n \ n ` );
2026-05-10 19:13:06 +02:00
currentBlockIndex ++ ;
textBlockOpen = false ;
2026-05-10 10:46:41 +02:00
}
2026-05-10 19:13:06 +02:00
// Keine Web-Suchen → finale Events senden
if ( iterWebSearches . length === 0 ) {
2026-05-10 10:46:41 +02:00
res . write ( ` event: message_delta \n data: ${ JSON . stringify ( {
type : 'message_delta' ,
2026-05-10 11:11:59 +02:00
delta : { stop _reason : emittedToolUse ? 'tool_use' : 'end_turn' } ,
2026-05-10 19:13:06 +02:00
usage : { output _tokens : evalCount }
2026-05-10 10:46:41 +02:00
} ) } \ n \ n ` );
2026-05-10 11:11:59 +02:00
res . write ( ` event: message_stop \n data: ${ JSON . stringify ( { type : 'message_stop' } )} \n \n ` ) ;
2026-05-10 10:46:41 +02:00
console . log ( ` ${ colors . green } ✓ ${ colors . reset } \n ` ) ;
2026-05-10 19:13:06 +02:00
break ;
2026-05-10 10:46:41 +02:00
}
2026-05-10 19:13:06 +02:00
// Web-Suchen ausführen und Ergebnisse in Messages einbauen
ollamaBody . messages . push ( {
role : 'assistant' ,
content : iterContent ,
tool _calls : iterAllToolCalls
} ) ;
2026-05-10 10:46:41 +02:00
2026-05-10 19:13:06 +02:00
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 || '' ) ;
2026-05-10 10:46:41 +02:00
}
2026-05-10 19:13:06 +02:00
ollamaBody . messages . push ( { role : 'tool' , content : result ? ? ` Tool ' ${ name } ' returned no result. ` } ) ;
2026-05-10 10:46:41 +02:00
}
2026-05-10 19:13:06 +02:00
// 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 )
} ) ;
2026-05-10 10:46:41 +02:00
2026-05-10 19:13:06 +02:00
if ( ! currentResponse . ok ) {
const errText = await currentResponse . text ( ) ;
throw new Error ( ` Ollama: ${ currentResponse . status } ${ errText } ` ) ;
2026-05-10 10:46:41 +02:00
}
}
res . end ( ) ;
}
2026-05-10 11:11:59 +02:00
// ── Proxy-Endpunkt ────────────────────────────────────────────────────────────
2026-05-10 10:46:41 +02:00
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-' ) ) {
2026-05-10 11:11:59 +02:00
anthropicBody . model = OLLAMA _MODEL ;
2026-05-10 10:46:41 +02:00
}
const ollamaBody = convertAnthropicToOllama ( anthropicBody ) ;
console . log (
2026-05-10 19:13:06 +02:00
` ${ colors . magenta } [msgs= ${ ollamaBody . messages . length } , tools= ${ ollamaBody . tools . length } , ctx=256k, think=false] ${ colors . reset } `
2026-05-10 10:46:41 +02:00
) ;
2026-05-10 11:11:59 +02:00
const response = await fetch ( OLLAMA _URL , {
2026-05-10 10:46:41 +02:00
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
2026-05-10 11:11:59 +02:00
'Authorization' : ` Bearer ${ OLLAMA _AUTH } `
2026-05-10 10:46:41 +02:00
} ,
body : JSON . stringify ( ollamaBody )
} ) ;
if ( ! response . ok ) {
const errorText = await response . text ( ) ;
2026-05-10 19:13:06 +02:00
throw new Error ( ` Ollama: ${ response . status } ${ errorText } ` ) ;
2026-05-10 10:46:41 +02:00
}
2026-05-10 19:13:06 +02:00
return handleResponse ( response , anthropicBody , res , requestNum , ollamaBody ) ;
2026-05-10 10:46:41 +02:00
} catch ( error ) {
console . error ( ` ${ colors . red } ${ error . message } ${ colors . reset } ` ) ;
if ( ! res . headersSent ) {
res . status ( 500 ) . json ( {
type : 'error' ,
2026-05-10 11:11:59 +02:00
error : { type : 'api_error' , message : error . message }
2026-05-10 10:46:41 +02:00
} ) ;
} else {
res . end ( ) ;
}
}
} ) ;
2026-05-10 11:11:59 +02:00
app . listen ( PORT , ( ) => {
console . log ( ` ${ colors . magenta } noThinkProxy: localhost: ${ PORT } ${ colors . reset } ` ) ;
2026-05-10 19:13:06 +02:00
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 ` ) ;
2026-05-10 10:46:41 +02:00
} ) ;