/** * Tool Format Converter * Converts between Claude and Ollama tool/function formats */ /** * Convert Claude tool definition to Ollama function format * * Claude format: * { name: "...", description: "...", input_schema: { type: "object", properties: {...} } } * * Ollama format: * { type: "function", function: { name: "...", description: "...", parameters: {...} } } * * @param {object} claudeTool - Tool in Claude format * @returns {object} Tool in Ollama format */ export function claudeToolToOllama(claudeTool) { return { type: "function", function: { name: claudeTool.name, description: claudeTool.description, parameters: claudeTool.input_schema } }; } /** * Convert array of Claude tools to Ollama format * @param {Array} claudeTools - Array of Claude tool definitions * @returns {Array} Array of Ollama function definitions */ export function claudeToolsToOllama(claudeTools) { if (!claudeTools || !Array.isArray(claudeTools)) { return []; } return claudeTools.map(claudeToolToOllama); } /** * Convert Ollama tool call to Claude format * * Ollama format (in message): * { tool_calls: [{ function: { name: "...", arguments: {...} } }] } * * Claude format: * { type: "tool_use", id: "...", name: "...", input: {...} } * * @param {object} ollamaToolCall - Tool call from Ollama response * @param {number} index - Index for generating unique ID * @returns {object} Tool call in Claude format */ export function ollamaToolCallToClaude(ollamaToolCall, index = 0) { const func = ollamaToolCall.function; // Parse arguments if it's a string let input = func.arguments; if (typeof input === 'string') { try { input = JSON.parse(input); } catch (e) { console.warn('[ToolConverter] Failed to parse tool arguments:', e); input = {}; } } return { type: "tool_use", id: `toolu_ollama_${Date.now()}_${index}`, name: func.name, input: input || {} }; } /** * Convert Claude tool result to Ollama format * * Claude format (in messages): * { role: "user", content: [{ type: "tool_result", tool_use_id: "...", content: "..." }] } * * Ollama format: * { role: "tool", content: "...", name: "..." } * * @param {object} claudeToolResult - Tool result in Claude format * @param {string} toolName - Name of the tool (from previous tool_use) * @returns {object} Tool result in Ollama message format */ export function claudeToolResultToOllama(claudeToolResult, toolName) { let content = claudeToolResult.content; // Stringify if it's an object if (typeof content === 'object') { content = JSON.stringify(content); } return { role: "tool", content: content, name: toolName }; } /** * Convert Claude messages array to Ollama format * Handles regular messages and tool result messages * * @param {Array} claudeMessages - Messages in Claude format * @param {string} systemPrompt - System prompt to prepend * @returns {Array} Messages in Ollama format */ export function claudeMessagesToOllama(claudeMessages, systemPrompt) { const ollamaMessages = []; // Add system message if provided if (systemPrompt) { ollamaMessages.push({ role: "system", content: systemPrompt }); } // Track tool names for tool results const toolNameMap = new Map(); for (const msg of claudeMessages) { if (msg.role === 'user') { // Check if it's a tool result message if (Array.isArray(msg.content)) { for (const block of msg.content) { if (block.type === 'tool_result') { const toolName = toolNameMap.get(block.tool_use_id) || 'unknown'; ollamaMessages.push(claudeToolResultToOllama(block, toolName)); } else if (block.type === 'text') { ollamaMessages.push({ role: "user", content: block.text }); } } } else { ollamaMessages.push({ role: "user", content: msg.content }); } } else if (msg.role === 'assistant') { // Handle assistant messages with potential tool calls if (Array.isArray(msg.content)) { let textContent = ''; const toolCalls = []; for (const block of msg.content) { if (block.type === 'text') { textContent += block.text; } else if (block.type === 'tool_use') { // Track tool name for later tool results toolNameMap.set(block.id, block.name); toolCalls.push({ function: { name: block.name, // Ollama expects arguments as object, not string arguments: block.input || {} } }); } } const assistantMsg = { role: "assistant", content: textContent || "" }; if (toolCalls.length > 0) { assistantMsg.tool_calls = toolCalls; } ollamaMessages.push(assistantMsg); } else { ollamaMessages.push({ role: "assistant", content: msg.content }); } } } return ollamaMessages; } /** * Convert Ollama response to Claude format * * @param {object} ollamaResponse - Response from Ollama API * @returns {object} Response in Claude format */ export function ollamaResponseToClaude(ollamaResponse) { const content = []; const message = ollamaResponse.message; // Add text content if present if (message.content) { content.push({ type: "text", text: message.content }); } // Add tool calls if present if (message.tool_calls && message.tool_calls.length > 0) { for (let i = 0; i < message.tool_calls.length; i++) { content.push(ollamaToolCallToClaude(message.tool_calls[i], i)); } } // Determine stop reason let stopReason = "end_turn"; if (message.tool_calls && message.tool_calls.length > 0) { stopReason = "tool_use"; } else if (ollamaResponse.done_reason === "length") { stopReason = "max_tokens"; } return { id: `msg_ollama_${Date.now()}`, type: "message", role: "assistant", content: content, model: ollamaResponse.model, stop_reason: stopReason, usage: { input_tokens: ollamaResponse.prompt_eval_count || 0, output_tokens: ollamaResponse.eval_count || 0 } }; } export default { claudeToolToOllama, claudeToolsToOllama, ollamaToolCallToClaude, claudeToolResultToOllama, claudeMessagesToOllama, ollamaResponseToClaude };