initiale version
This commit is contained in:
290
app.js.v10
Normal file
290
app.js.v10
Normal file
@@ -0,0 +1,290 @@
|
||||
// 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`);
|
||||
});
|
||||
Reference in New Issue
Block a user