Add local network IP binding for federation support

- Add get_local_ip() function to detect local network IP (192.x.x.x or 100.x.x.x)
- Bind server to specific local IP instead of 0.0.0.0 for security
- Only expose to local network, not internet
- Fall back to localhost if not on private network

This enables federation between multiple Macs on the same local network
while keeping the server secure from external access.
This commit is contained in:
2026-02-24 04:07:27 +01:00
parent d30eedaa63
commit 47f6c8e7d9
2 changed files with 81 additions and 17 deletions
+55 -16
View File
@@ -51,13 +51,36 @@ def format_tool_description(tool) -> str:
def format_messages_with_tools(messages: list, tools: Optional[list] = None) -> str:
"""Format chat messages into a single prompt using ChatML format.
Note: Tools are currently ignored - the model will respond normally.
"""
"""Format chat messages into a single prompt using ChatML format."""
formatted = []
# Tools are accepted but ignored for now - model responds normally
# Add tool instructions if tools are present
if tools:
tool_instructions = """You have access to tools. When asked to create, write, or modify files, USE the tools.
To use a tool, output ONLY valid JSON in this exact format:
{"tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "write", "arguments": "{\\"filePath\\": \"filename.py\\", \\"content\\": \"print('hello')\\"}"}}]}
The "arguments" field must be a JSON string (serialized) with escaped quotes.
Available tools:"""
for tool in tools:
tool_instructions += "\n" + format_tool_description(tool)
tool_instructions += "\n\nDO NOT explain how to do things. DO use tools to actually do them. Output ONLY the JSON when using tools."
# Add to system message or create one
has_system = False
for msg in messages:
if msg.role == "system":
msg.content = tool_instructions + "\n\n" + (msg.content or "")
has_system = True
break
if not has_system:
from api.models import ChatMessage
messages.insert(0, ChatMessage(role="system", content=tool_instructions))
for msg in messages:
role = msg.role
@@ -86,18 +109,34 @@ def parse_tool_calls(text: str) -> tuple:
import json
import re
# Try to find JSON with tool_calls
# Try to find JSON with tool_calls (handle both quoted and unquoted)
try:
# Look for JSON object with tool_calls
json_match = re.search(r'\{[^}]*"tool_calls"[^}]*\}', text, re.DOTALL)
if json_match:
data = json.loads(json_match.group())
if "tool_calls" in data:
tool_calls = data["tool_calls"]
# Remove the JSON from the text
content = text[:json_match.start()].strip()
return content, tool_calls
except (json.JSONDecodeError, AttributeError):
# Look for tool_calls pattern (with or without quotes, with or without braces)
pattern = r'tool_calls\s*:\s*(\[.*?\])(?=\s*\}|\s*$)'
match = re.search(pattern, text, re.DOTALL)
if match:
array_str = match.group(1)
# Try to parse as JSON
try:
tool_calls = json.loads(array_str)
except json.JSONDecodeError:
# Handle JavaScript-style objects with unquoted keys
fixed = array_str
# Replace unquoted keys with quoted keys
fixed = re.sub(r'([{,])\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:', r'\1"\2":', fixed)
# Replace single quotes with double quotes
fixed = fixed.replace("'", '"')
tool_calls = json.loads(fixed)
# Find and remove the tool_calls section from text
full_pattern = r'\{?\s*tool_calls\s*:\s*\[.*?\]\s*\}?'
full_match = re.search(full_pattern, text, re.DOTALL)
if full_match:
content = text[:full_match.start()].strip()
else:
content = text[:match.start()].strip()
return content, tool_calls
except (Exception):
pass
# Try alternative format: look for function call patterns