Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 109 additions & 1 deletion backend/app/agent/agent_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import logging
import uuid
from collections.abc import Callable
from copy import deepcopy
from typing import Any

from camel.messages import BaseMessage
Expand All @@ -35,6 +36,106 @@
from app.service.task import ActionCreateAgentData, Agents, get_task_lock
from app.utils.event_loop_utils import _schedule_async_task


def _strip_strict_from_tools_for_anthropic(
tools: list[FunctionTool | Callable] | None,
model_platform: str,
) -> list[FunctionTool | Callable] | None:
"""Strip 'strict' from tool schemas for Anthropic models.

Anthropic's API does not support OpenAI's 'strict' mode for tool calling.
CAMEL's FunctionTool adds 'strict': True by default, which causes
400 errors when passed to Anthropic models ("does not support strict tools").

Also ensures `additionalProperties: false` on all objects for Groq compatibility.

Args:
tools: List of FunctionTool or callable tools
model_platform: The model platform string (e.g., "anthropic", "groq")

Returns:
Modified tools list with 'strict' removed from schemas, or None if input was None
"""
if not tools:
return tools

platform = model_platform.lower()
needs_strict_removal = platform == "anthropic"
needs_additional_props_false = platform in ("groq", "anthropic")

if not needs_strict_removal and not needs_additional_props_false:
return tools

def _ensure_additional_props_false(obj: Any) -> Any:
"""Recursively ensure additionalProperties: false on all object schemas."""
if isinstance(obj, dict):
new_obj = {}
for k, v in obj.items():
if (
k == "type"
and v == "object"
and "additionalProperties" not in obj
):
new_obj[k] = v
new_obj["additionalProperties"] = False
elif k in ("properties", "$defs") and isinstance(v, dict):
new_obj[k] = {
pk: _ensure_additional_props_false(pv)
for pk, pv in v.items()
}
elif k in ("items", "allOf", "oneOf", "anyOf") and isinstance(
v, (dict, list)
):
if isinstance(v, dict):
new_obj[k] = _ensure_additional_props_false(v)
else:
new_obj[k] = [
_ensure_additional_props_false(item) for item in v
]
else:
new_obj[k] = _ensure_additional_props_false(v)
return new_obj
elif isinstance(obj, list):
return [_ensure_additional_props_false(item) for item in obj]
return obj

stripped_tools = []
for tool in tools:
if isinstance(tool, FunctionTool):
schema = tool.get_openai_tool_schema()
if isinstance(schema, dict) and "function" in schema:
func_schema = schema["function"]
new_func_schema = deepcopy(func_schema)
modified = False

if needs_strict_removal and "strict" in new_func_schema:
del new_func_schema["strict"]
modified = True

if needs_additional_props_false:
if "parameters" in new_func_schema:
new_func_schema["parameters"] = (
_ensure_additional_props_false(
new_func_schema["parameters"]
)
)
modified = True

if modified:
new_schema = {
"type": "function",
"function": new_func_schema,
}
new_tool = FunctionTool(
tool.func, openai_tool_schema=new_schema
)
stripped_tools.append(new_tool)
continue
stripped_tools.append(tool)

return stripped_tools


# OpenAI chat-completions streaming only returns token usage when
# `stream_options.include_usage` is requested. Without it the request-level
# usage callback (on_request_usage) fires with 0 tokens, and because the
Expand Down Expand Up @@ -295,12 +396,19 @@ def build_model(force_refresh: bool = False):

model = build_model()

# Strip 'strict' from tool schemas for Anthropic models
# (CAMEL adds strict=True by default, which causes 400 errors)
effective_platform = effective_config.get("model_platform", "")
tools_for_agent = _strip_strict_from_tools_for_anthropic(
tools, effective_platform
)

return ListenChatAgent(
options.project_id,
agent_name,
system_message,
model=model,
tools=tools,
tools=tools_for_agent,
agent_id=agent_id,
prune_tool_calls_from_memory=prune_tool_calls_from_memory,
toolkits_to_register_agent=toolkits_to_register_agent,
Expand Down
3 changes: 2 additions & 1 deletion backend/app/model/model_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from pydantic import BeforeValidator

PLATFORM_ALIAS_MAPPING: Final[dict[str, str]] = {
"z.ai": "zhipuai",
"z.ai": "openai-compatible-model",
"deepseek": "openai-compatible-model",
"ModelArk": "openai-compatible-model",
"grok": "openai-compatible-model",
"ernie": "qianfan",
Expand Down
3 changes: 2 additions & 1 deletion backend/tests/app/model/test_model_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@

def test_normalize_model_platform_maps_known_aliases():
assert normalize_model_platform("grok") == "openai-compatible-model"
assert normalize_model_platform("z.ai") == "zhipuai"
assert normalize_model_platform("z.ai") == "openai-compatible-model"
assert normalize_model_platform("deepseek") == "openai-compatible-model"
assert normalize_model_platform("ModelArk") == "openai-compatible-model"
assert normalize_model_platform("ernie") == "qianfan"
assert normalize_model_platform("llama.cpp") == "openai-compatible-model"
Expand Down
9 changes: 6 additions & 3 deletions server/.env.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
debug=false
url_prefix=/api
secret_key=postgres
secret_key=CHANGE_ME
# Optional but recommended: set a distinct issuer per deployed environment
# (for example,https://dev.eigent.ai).
# If omitted, the server derives a non-secret environment fingerprint from
# SERVER_URL/VITE_* URL settings and database_url.
# TOKEN_ISSUER=https://dev.eigent.ai
TOKEN_AUDIENCE=eigent-api
# Chat Share Secret Key
CHAT_SHARE_SECRET_KEY=put-your-secret-key-here
CHAT_SHARE_SALT=put-your-encode-salt-here
CHAT_SHARE_SECRET_KEY=CHANGE_ME
CHAT_SHARE_SALT=CHANGE_ME

# Database password for docker-compose
POSTGRES_PASSWORD=CHANGE_ME

# Remote control public web origin. Set this to the HTTPS site users open
# from a phone or another computer, not the local desktop server.
Expand Down
7 changes: 7 additions & 0 deletions server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ wheels/
# Virtual environments
.venv

# Environment files
.env
.env.*

# Alembic config (contains database credentials)
alembic.ini

runtime

app/public/upload/
41 changes: 22 additions & 19 deletions server/app/model/chat/chat_share.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,31 @@
logger = logging.getLogger(__name__)


def _get_secret_key() -> str:
"""Return the share-token signing key.

Falls back to a random ephemeral key when the environment variable
is not set. A hardcoded default must never be used because the
source code is public and anyone could forge valid share tokens.
"""
key = os.getenv("CHAT_SHARE_SECRET_KEY")
if key:
return key
logger.warning(
"CHAT_SHARE_SECRET_KEY not set — using a random ephemeral key. "
"Share links will not survive server restarts. "
"Set the CHAT_SHARE_SECRET_KEY environment variable for persistence."
def _get_secret_key() -> str:
"""Return the share-token signing key.

Raises an error if the environment variable is not set.
A hardcoded default or random fallback must never be used because the
source code is public and anyone could forge valid share tokens.
"""
key = os.getenv("CHAT_SHARE_SECRET_KEY")
if key:
return key
raise RuntimeError(
"CHAT_SHARE_SECRET_KEY environment variable is required but not set. "
"Generate a secure key with: openssl rand -base64 32"
)
return secrets.token_urlsafe(32)


def _get_salt() -> str:
salt = os.getenv("CHAT_SHARE_SALT")
if salt:
return salt
def _get_salt() -> str:
salt = os.getenv("CHAT_SHARE_SALT")
if salt:
return salt
logger.warning(
"CHAT_SHARE_SALT not set — using a random ephemeral salt. "
"Share links will not survive server restarts. "
"Set the CHAT_SHARE_SALT environment variable for persistence."
)
return secrets.token_urlsafe(8)


Expand Down
2 changes: 1 addition & 1 deletion server/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
environment:
POSTGRES_DB: eigent
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123456
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-123456}
POSTGRES_INITDB_ARGS: '--encoding=UTF-8 --lc-collate=C --lc-ctype=C'
ports:
- '5432:5432'
Expand Down
9 changes: 4 additions & 5 deletions server/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
services:
# PostgreSQL Database
# PostgreSQL Database
postgres:
image: postgres:15
container_name: eigent_postgres
restart: unless-stopped
environment:
POSTGRES_DB: eigent
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123456
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-123456}
POSTGRES_INITDB_ARGS: '--encoding=UTF-8 --lc-collate=C --lc-ctype=C'
ports:
- '5432:5432'
Expand Down Expand Up @@ -44,15 +43,15 @@ services:
context: ..
dockerfile: server/Dockerfile
args:
database_url: postgresql://postgres:123456@postgres:5432/eigent
database_url: ${DATABASE_URL:-postgresql://postgres:123456@postgres:5432/eigent}
container_name: eigent_api
restart: unless-stopped
ports:
- '3001:5678'
env_file:
- .env
environment:
- database_url=postgresql://postgres:123456@postgres:5432/eigent
- database_url=${DATABASE_URL:-postgresql://postgres:123456@postgres:5432/eigent}
- redis_url=redis://redis:6379/0
- celery_broker_url=redis://redis:6379/0
- celery_result_url=redis://redis:6379/0
Expand Down
Loading