- Add Cloudflare Workers AI as third provider alongside Claude and Ollama - New cloudflare.js API handler with format conversion - Tool converter functions for Cloudflare's OpenAI-compatible format - Handle [TOOL_CALLS] and [Called tool:] text formats from Mistral - Robust parser that handles truncated JSON responses - Add usage tracking with cost display - New usageTracker.js service for tracking token usage per session - UsageDetailModal component showing per-request breakdown - Cost display in ChatPanel header - Add new diagram manipulation features - Entity scale and rotation support via modify_entity tool - Wikipedia search tool for researching topics before diagramming - Clear conversation tool to reset chat history - JSON import from hamburger menu (moved from ChatPanel) - Fix connection label rotation in billboard mode - Labels no longer have conflicting local rotation when billboard enabled - Update rotation when rendering mode changes - Improve tool calling reliability - Add MAX_TOOL_ITERATIONS safety limit - Break loop after model switch to prevent context issues - Increase max_tokens to 4096 to prevent truncation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
664 lines
22 KiB
JavaScript
664 lines
22 KiB
JavaScript
/**
|
|
* 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
|
|
}
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// Cloudflare Workers AI Converters
|
|
// ============================================
|
|
|
|
/**
|
|
* Convert Claude tool definition to Cloudflare format
|
|
* Cloudflare uses OpenAI-compatible format
|
|
*
|
|
* Claude format:
|
|
* { name: "...", description: "...", input_schema: { type: "object", properties: {...} } }
|
|
*
|
|
* Cloudflare format:
|
|
* { type: "function", function: { name: "...", description: "...", parameters: {...} } }
|
|
*
|
|
* @param {object} claudeTool - Tool in Claude format
|
|
* @returns {object} Tool in Cloudflare format
|
|
*/
|
|
export function claudeToolToCloudflare(claudeTool) {
|
|
return {
|
|
type: "function",
|
|
function: {
|
|
name: claudeTool.name,
|
|
description: claudeTool.description,
|
|
parameters: claudeTool.input_schema
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert array of Claude tools to Cloudflare format
|
|
* @param {Array} claudeTools - Array of Claude tool definitions
|
|
* @returns {Array} Array of Cloudflare function definitions
|
|
*/
|
|
export function claudeToolsToCloudflare(claudeTools) {
|
|
if (!claudeTools || !Array.isArray(claudeTools)) {
|
|
return [];
|
|
}
|
|
return claudeTools.map(claudeToolToCloudflare);
|
|
}
|
|
|
|
/**
|
|
* Convert Cloudflare tool call to Claude format
|
|
*
|
|
* Cloudflare format:
|
|
* { name: "...", arguments: {...} }
|
|
*
|
|
* Claude format:
|
|
* { type: "tool_use", id: "...", name: "...", input: {...} }
|
|
*
|
|
* @param {object} cfToolCall - Tool call from Cloudflare response
|
|
* @param {number} index - Index for generating unique ID
|
|
* @returns {object} Tool call in Claude format
|
|
*/
|
|
export function cloudflareToolCallToClaude(cfToolCall, index = 0) {
|
|
// Parse arguments if it's a string
|
|
let input = cfToolCall.arguments;
|
|
if (typeof input === 'string') {
|
|
try {
|
|
input = JSON.parse(input);
|
|
} catch (e) {
|
|
console.warn('[ToolConverter] Failed to parse Cloudflare tool arguments:', e);
|
|
input = {};
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: "tool_use",
|
|
id: `toolu_cf_${Date.now()}_${index}`,
|
|
name: cfToolCall.name,
|
|
input: input || {}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert Claude messages array to Cloudflare format
|
|
* Cloudflare uses OpenAI-compatible message format
|
|
*
|
|
* IMPORTANT: Cloudflare Workers AI does NOT support multi-turn tool conversations.
|
|
* It crashes with error 3043 when conversation history contains tool_calls or tool results.
|
|
* We must strip tool call history and only keep text content from past messages.
|
|
*
|
|
* @param {Array} claudeMessages - Messages in Claude format
|
|
* @param {string} systemPrompt - System prompt to prepend
|
|
* @returns {Array} Messages in Cloudflare format
|
|
*/
|
|
export function claudeMessagesToCloudflare(claudeMessages, systemPrompt) {
|
|
const cfMessages = [];
|
|
|
|
// Add system message if provided
|
|
if (systemPrompt) {
|
|
cfMessages.push({
|
|
role: "system",
|
|
content: systemPrompt
|
|
});
|
|
}
|
|
|
|
// Cloudflare doesn't support tool call history in native format - convert to text
|
|
// so the model knows what tools were called and their results
|
|
for (const msg of claudeMessages) {
|
|
if (msg.role === 'user') {
|
|
if (Array.isArray(msg.content)) {
|
|
// Convert tool_result blocks to text summaries
|
|
const textParts = [];
|
|
for (const block of msg.content) {
|
|
if (block.type === 'text') {
|
|
textParts.push(block.text);
|
|
} else if (block.type === 'tool_result') {
|
|
// Convert tool result to readable text so model knows it was executed
|
|
textParts.push(`[Tool Result: ${block.content}]`);
|
|
}
|
|
}
|
|
if (textParts.length > 0) {
|
|
cfMessages.push({
|
|
role: "user",
|
|
content: textParts.join('\n')
|
|
});
|
|
}
|
|
} else {
|
|
cfMessages.push({
|
|
role: "user",
|
|
content: msg.content
|
|
});
|
|
}
|
|
} else if (msg.role === 'assistant') {
|
|
// For assistant messages, convert tool_use to text descriptions
|
|
const textParts = [];
|
|
|
|
if (Array.isArray(msg.content)) {
|
|
for (const block of msg.content) {
|
|
if (block.type === 'text') {
|
|
textParts.push(block.text);
|
|
} else if (block.type === 'tool_use') {
|
|
// Convert tool call to readable text so model knows it called this
|
|
const argsStr = JSON.stringify(block.input || {});
|
|
textParts.push(`[Called tool: ${block.name}(${argsStr})]`);
|
|
}
|
|
}
|
|
} else {
|
|
textParts.push(msg.content || '');
|
|
}
|
|
|
|
// Also handle pre-converted messages that might have tool_calls property
|
|
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
for (const tc of msg.tool_calls) {
|
|
const name = tc.function?.name || tc.name || 'unknown';
|
|
const args = tc.function?.arguments || tc.arguments || '{}';
|
|
textParts.push(`[Called tool: ${name}(${typeof args === 'string' ? args : JSON.stringify(args)})]`);
|
|
}
|
|
}
|
|
|
|
const textContent = textParts.filter(t => t).join('\n');
|
|
if (textContent) {
|
|
cfMessages.push({
|
|
role: "assistant",
|
|
content: textContent
|
|
});
|
|
}
|
|
} else if (msg.role === 'tool') {
|
|
// Convert tool messages to user messages with result text
|
|
cfMessages.push({
|
|
role: "user",
|
|
content: `[Tool Result (${msg.name || 'unknown'}): ${msg.content}]`
|
|
});
|
|
}
|
|
}
|
|
|
|
return cfMessages;
|
|
}
|
|
|
|
/**
|
|
* Try to repair and parse a potentially truncated JSON object
|
|
* @param {string} jsonStr - Potentially incomplete JSON string
|
|
* @returns {object|null} - Parsed object or null if unparseable
|
|
*/
|
|
function tryRepairAndParse(jsonStr) {
|
|
// First try as-is
|
|
try {
|
|
return JSON.parse(jsonStr);
|
|
} catch (e) {
|
|
// Try adding closing brackets
|
|
const repairs = [
|
|
jsonStr + '}',
|
|
jsonStr + '"}',
|
|
jsonStr + '}}',
|
|
jsonStr + '"}}',
|
|
jsonStr + ': null}}',
|
|
jsonStr + '": null}}'
|
|
];
|
|
|
|
for (const attempt of repairs) {
|
|
try {
|
|
const parsed = JSON.parse(attempt);
|
|
if (parsed.name) { // Must have a name to be valid
|
|
return parsed;
|
|
}
|
|
} catch (e2) {
|
|
// Continue trying
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse tool calls from text response
|
|
* Handles multiple formats:
|
|
* 1. Mistral native: [TOOL_CALLS][{"name": "...", "arguments": {...}}, ...]
|
|
* 2. History format: [Called tool: name({args})]
|
|
*
|
|
* This parser is resilient to truncation - it will extract as many valid tool calls
|
|
* as possible even if the JSON is incomplete.
|
|
*
|
|
* @param {string} text - Text response that may contain embedded tool calls
|
|
* @returns {object} - { cleanText: string, toolCalls: array }
|
|
*/
|
|
function parseTextToolCalls(text) {
|
|
if (!text) return { cleanText: '', toolCalls: [] };
|
|
|
|
const toolCalls = [];
|
|
let cleanText = text;
|
|
|
|
// Format 1: [TOOL_CALLS][...] (Mistral native format)
|
|
const toolCallMatch = text.match(/\[TOOL_CALLS\]\s*(\[[\s\S]*)/);
|
|
|
|
if (toolCallMatch) {
|
|
const toolCallsJson = toolCallMatch[1];
|
|
|
|
// First try normal JSON.parse (for complete responses)
|
|
try {
|
|
const parsedCalls = JSON.parse(toolCallsJson);
|
|
if (Array.isArray(parsedCalls)) {
|
|
const validCalls = parsedCalls
|
|
.filter(call => call && call.name)
|
|
.map(call => ({
|
|
name: call.name,
|
|
arguments: call.arguments || {}
|
|
}));
|
|
|
|
console.log(`[ToolConverter] Parsed ${validCalls.length} tool calls from [TOOL_CALLS] JSON`);
|
|
cleanText = text.replace(/\[TOOL_CALLS\]\s*\[[\s\S]*/, '').trim();
|
|
return { cleanText, toolCalls: validCalls };
|
|
}
|
|
} catch (e) {
|
|
console.log('[ToolConverter] [TOOL_CALLS] JSON incomplete, attempting to extract individual tool calls...');
|
|
}
|
|
|
|
// JSON is truncated - extract individual tool calls using regex
|
|
const toolCallStarts = [];
|
|
const startPattern = /\{"name"\s*:\s*"/g;
|
|
let match;
|
|
|
|
while ((match = startPattern.exec(toolCallsJson)) !== null) {
|
|
toolCallStarts.push(match.index);
|
|
}
|
|
|
|
console.log(`[ToolConverter] Found ${toolCallStarts.length} potential tool call starts in [TOOL_CALLS]`);
|
|
|
|
for (let i = 0; i < toolCallStarts.length; i++) {
|
|
const start = toolCallStarts[i];
|
|
const end = toolCallStarts[i + 1] || toolCallsJson.length;
|
|
let segment = toolCallsJson.substring(start, end).replace(/,\s*$/, '');
|
|
const parsed = tryRepairAndParse(segment);
|
|
|
|
if (parsed && parsed.name) {
|
|
toolCalls.push({
|
|
name: parsed.name,
|
|
arguments: parsed.arguments || {}
|
|
});
|
|
console.log(`[ToolConverter] Extracted tool call from [TOOL_CALLS]: ${parsed.name}`);
|
|
}
|
|
}
|
|
|
|
cleanText = text.replace(/\[TOOL_CALLS\]\s*\[[\s\S]*/, '').trim();
|
|
if (toolCalls.length > 0) {
|
|
console.log(`[ToolConverter] Extracted ${toolCalls.length} tool calls from [TOOL_CALLS] format`);
|
|
return { cleanText, toolCalls };
|
|
}
|
|
}
|
|
|
|
// Format 2: [Called tool: name({args})] (history format the model might mimic)
|
|
// Match patterns like: [Called tool: create_entity({"shape": "box", ...})]
|
|
const calledToolPattern = /\[Called tool:\s*(\w+)\((\{[\s\S]*?\})\)\]/g;
|
|
let calledMatch;
|
|
const calledToolMatches = [];
|
|
|
|
while ((calledMatch = calledToolPattern.exec(text)) !== null) {
|
|
calledToolMatches.push({
|
|
fullMatch: calledMatch[0],
|
|
name: calledMatch[1],
|
|
argsStr: calledMatch[2]
|
|
});
|
|
}
|
|
|
|
if (calledToolMatches.length > 0) {
|
|
console.log(`[ToolConverter] Found ${calledToolMatches.length} [Called tool:] format tool calls`);
|
|
|
|
for (const match of calledToolMatches) {
|
|
try {
|
|
const args = JSON.parse(match.argsStr);
|
|
toolCalls.push({
|
|
name: match.name,
|
|
arguments: args
|
|
});
|
|
console.log(`[ToolConverter] Extracted tool call from [Called tool:]: ${match.name}`);
|
|
// Remove this match from clean text
|
|
cleanText = cleanText.replace(match.fullMatch, '');
|
|
} catch (e) {
|
|
console.warn(`[ToolConverter] Failed to parse [Called tool:] args for ${match.name}:`, e.message);
|
|
// Try to repair the JSON
|
|
const repaired = tryRepairAndParse(match.argsStr);
|
|
if (repaired) {
|
|
toolCalls.push({
|
|
name: match.name,
|
|
arguments: repaired
|
|
});
|
|
console.log(`[ToolConverter] Repaired and extracted tool call: ${match.name}`);
|
|
cleanText = cleanText.replace(match.fullMatch, '');
|
|
}
|
|
}
|
|
}
|
|
|
|
cleanText = cleanText.trim();
|
|
if (toolCalls.length > 0) {
|
|
console.log(`[ToolConverter] Extracted ${toolCalls.length} tool calls from [Called tool:] format`);
|
|
return { cleanText, toolCalls };
|
|
}
|
|
}
|
|
|
|
// No tool calls found
|
|
return { cleanText: text, toolCalls: [] };
|
|
}
|
|
|
|
/**
|
|
* Convert Cloudflare response to Claude format
|
|
*
|
|
* Cloudflare response format:
|
|
* {
|
|
* result: {
|
|
* response: "text output",
|
|
* tool_calls: [{ name: "...", arguments: {...} }]
|
|
* },
|
|
* success: true
|
|
* }
|
|
*
|
|
* Note: Some models (like Mistral) output tool calls as text in format:
|
|
* [TOOL_CALLS][{...}]
|
|
*
|
|
* @param {object} cfResponse - Response from Cloudflare Workers AI API
|
|
* @param {string} model - Model name used
|
|
* @returns {object} Response in Claude format
|
|
*/
|
|
export function cloudflareResponseToClaude(cfResponse, model) {
|
|
const content = [];
|
|
const result = cfResponse.result || cfResponse;
|
|
|
|
// Get tool calls from proper field or parse from text
|
|
let toolCalls = result.tool_calls || [];
|
|
let textResponse = result.response || '';
|
|
|
|
// Log raw response for debugging
|
|
console.log(`[ToolConverter] Raw response (first 500 chars): ${textResponse.substring(0, 500)}`);
|
|
console.log(`[ToolConverter] Native tool_calls present: ${toolCalls.length}`);
|
|
|
|
// Check if tool calls are embedded in text response (Mistral format or history format)
|
|
if (toolCalls.length === 0 && textResponse) {
|
|
console.log(`[ToolConverter] No native tool_calls, parsing text response...`);
|
|
const parsed = parseTextToolCalls(textResponse);
|
|
console.log(`[ToolConverter] Parsed ${parsed.toolCalls.length} tool calls from text`);
|
|
if (parsed.toolCalls.length > 0) {
|
|
toolCalls = parsed.toolCalls;
|
|
textResponse = parsed.cleanText;
|
|
}
|
|
}
|
|
|
|
// Add text content if present (after removing tool calls)
|
|
if (textResponse) {
|
|
content.push({
|
|
type: "text",
|
|
text: textResponse
|
|
});
|
|
}
|
|
|
|
// Add tool calls if present
|
|
if (toolCalls.length > 0) {
|
|
for (let i = 0; i < toolCalls.length; i++) {
|
|
content.push(cloudflareToolCallToClaude(toolCalls[i], i));
|
|
}
|
|
}
|
|
|
|
// Determine stop reason
|
|
let stopReason = "end_turn";
|
|
if (toolCalls.length > 0) {
|
|
stopReason = "tool_use";
|
|
}
|
|
|
|
// Extract usage if available
|
|
const usage = {
|
|
input_tokens: result.usage?.prompt_tokens || result.usage?.input_tokens || 0,
|
|
output_tokens: result.usage?.completion_tokens || result.usage?.output_tokens || 0
|
|
};
|
|
|
|
return {
|
|
id: `msg_cf_${Date.now()}`,
|
|
type: "message",
|
|
role: "assistant",
|
|
content: content,
|
|
model: model,
|
|
stop_reason: stopReason,
|
|
usage: usage
|
|
};
|
|
}
|
|
|
|
export default {
|
|
claudeToolToOllama,
|
|
claudeToolsToOllama,
|
|
ollamaToolCallToClaude,
|
|
claudeToolResultToOllama,
|
|
claudeMessagesToOllama,
|
|
ollamaResponseToClaude,
|
|
// Cloudflare converters
|
|
claudeToolToCloudflare,
|
|
claudeToolsToCloudflare,
|
|
cloudflareToolCallToClaude,
|
|
claudeMessagesToCloudflare,
|
|
cloudflareResponseToClaude
|
|
};
|