Skip to content
Merged
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
1 change: 1 addition & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ WIKI_CONTENT_WRITE_BASE_URL=http://backend:8000
DATA_TABLE_CONFIG={"dingtalk":{"appKey":"YOUR_APP_KEY","appSecret":"YOUR_APP_secret","operatorId":"YOUR_OPERATOR_ID","userMapping":{"baseId":"YOUR_BASE_ID","sheetId":"YOUR_SHEET_ID"}}}

# Default team configuration for each mode
DEFAULT_TEAM_WEWORK=wegent-wework-hidden#default
DEFAULT_TEAM_CHAT=wegent-chat#default
DEFAULT_TEAM_KNOWLEDGE=wegent-notebook#default
DEFAULT_TEAM_TASK=wegent-wework#default
Expand Down
12 changes: 11 additions & 1 deletion backend/app/api/endpoints/adapter/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,13 @@ def get_compatible_models(
if isinstance(model_config, dict):
env = model_config.get("env", {})
model_type = env.get("model", "")
api_format = getattr(model_crd.spec.apiFormat, "value", None)
protocol = model_crd.spec.protocol
is_codex_compatible = (
api_format == "responses"
or protocol == "openai-responses"
or model_config.get("wire_api") == "responses"
)

# Filter compatible models
# Agno supports OpenAI, Claude and Gemini models
Expand All @@ -320,7 +327,10 @@ def get_compatible_models(
"gemini",
]:
compatible_models.append({"name": model_kind.name})
elif shell_type == "ClaudeCode" and model_type == "claude":
elif shell_type == "ClaudeCode" and (
model_type == "claude"
or (model_type == "openai" and is_codex_compatible)
):
compatible_models.append({"name": model_kind.name})
except Exception as e:
logger.warning(f"Failed to parse model {model_kind.name}: {e}")
Expand Down
1 change: 1 addition & 0 deletions backend/app/api/endpoints/adapter/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def _get_default_teams_config() -> Dict[str, Dict[str, str]]:
"""
config = {}
mode_settings = {
"wework": settings.DEFAULT_TEAM_WEWORK,
"chat": settings.DEFAULT_TEAM_CHAT,
"code": settings.DEFAULT_TEAM_CODE,
"knowledge": settings.DEFAULT_TEAM_KNOWLEDGE,
Expand Down
2 changes: 2 additions & 0 deletions backend/app/api/endpoints/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ class DefaultTeamConfig(BaseModel):
class DefaultTeamsResponse(BaseModel):
"""Response model for default teams configuration"""

wework: Optional[DefaultTeamConfig] = None
chat: Optional[DefaultTeamConfig] = None
code: Optional[DefaultTeamConfig] = None
knowledge: Optional[DefaultTeamConfig] = None
Expand Down Expand Up @@ -607,6 +608,7 @@ async def get_default_teams(
from app.core.config import settings

return DefaultTeamsResponse(
wework=parse_default_team_config(settings.DEFAULT_TEAM_WEWORK),
chat=parse_default_team_config(settings.DEFAULT_TEAM_CHAT),
code=parse_default_team_config(settings.DEFAULT_TEAM_CODE),
knowledge=parse_default_team_config(settings.DEFAULT_TEAM_KNOWLEDGE),
Expand Down
3 changes: 3 additions & 0 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,9 @@ def parse_rag_runtime_mode(cls, v: Any) -> str | dict[str, str]:
DEFAULT_TEAM_TASK: str = (
"wegent-wework#default" # Default team for task mode (devices/chat page)
)
DEFAULT_TEAM_WEWORK: str = (
"wegent-wework-hidden#default" # Default hidden team for WeWork workbench
)

# JSON configuration for MCP servers (similar to Claude Desktop format)
# Example:
Expand Down
45 changes: 37 additions & 8 deletions backend/app/services/chat/trigger/unified.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,32 @@
SELECTED_KB_PRELOAD_SKILL = "wegent-knowledge"


def _reasoning_from_model_options(payload: Any) -> Optional[Dict[str, Any]]:
"""Convert UI model options into execution reasoning config."""
if payload is None:
return None
model_options = getattr(payload, "model_options", None)
if not isinstance(model_options, dict):
return None
reasoning = model_options.get("reasoning")
summary = model_options.get("summary")
if not reasoning and not summary:
return None

result: Dict[str, Any] = {}
if isinstance(reasoning, dict):
effort = reasoning.get("effort") or reasoning.get("reasoning")
summary = summary or reasoning.get("summary")
else:
effort = reasoning

if effort:
result["effort"] = str(effort)
if summary:
result["summary"] = str(summary)
return result or None


def _build_executor_attachment_payload(context: Any) -> dict[str, Any]:
"""Serialize an attachment context for executor-side downloading."""
return {
Expand Down Expand Up @@ -274,13 +300,16 @@ async def build_execution_request(
previous_bot_id=previous_bot_id,
)

# Merge reasoning_config from API request into model_config
# Priority: API request reasoning_config > model's think_config
if reasoning_config:
request.model_config["reasoning"] = reasoning_config
# Merge reasoning config from API/model selection into model_config.
# Priority: explicit API reasoning_config > UI model_options > model think_config.
selected_reasoning_config = reasoning_config or _reasoning_from_model_options(
payload
)
Comment on lines +303 to +307

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix priority logic to respect explicit empty reasoning_config.

The current code uses or, which treats an empty dict {} as falsy:

selected_reasoning_config = reasoning_config or _reasoning_from_model_options(payload)

If the API caller explicitly passes reasoning_config={} to indicate "no reasoning," the code incorrectly falls back to extracting reasoning from model_options, violating the documented priority ("explicit API reasoning_config > UI model_options").

🛠️ Proposed fix to honor explicit empty dict
-    # Merge reasoning config from API/model selection into model_config.
-    # Priority: explicit API reasoning_config > UI model_options > model think_config.
-    selected_reasoning_config = reasoning_config or _reasoning_from_model_options(
-        payload
-    )
+    # Merge reasoning config from API/model selection into model_config.
+    # Priority: explicit API reasoning_config > UI model_options > model think_config.
+    selected_reasoning_config = (
+        reasoning_config
+        if reasoning_config is not None
+        else _reasoning_from_model_options(payload)
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/chat/trigger/unified.py` around lines 303 - 307, The
current assignment uses truthiness and treats an explicit empty dict as falsy;
change it to explicitly prefer any provided reasoning_config (including {}) over
falling back to _reasoning_from_model_options(payload). Replace the
`selected_reasoning_config = reasoning_config or
_reasoning_from_model_options(payload)` logic with an explicit None check (e.g.,
if reasoning_config is not None then use reasoning_config else call
_reasoning_from_model_options(payload)) so that explicit empty dicts are
honored.

if selected_reasoning_config:
request.model_config["reasoning"] = selected_reasoning_config
logger.info(
"[build_execution_request] Applied reasoning config from API request: %s",
reasoning_config,
"[build_execution_request] Applied selected reasoning config: %s",
selected_reasoning_config,
)
elif request.model_config.get("think_config"):
# If no API reasoning_config but model has think_config, use it
Expand All @@ -291,8 +320,8 @@ async def build_execution_request(
)

# Store reasoning_config in ExecutionRequest for downstream access
request.reasoning_config = reasoning_config or request.model_config.get(
"reasoning"
request.reasoning_config = (
selected_reasoning_config or request.model_config.get("reasoning")
)

# Merge user-selected generate_params into videoConfig for video models
Expand Down
44 changes: 38 additions & 6 deletions backend/app/services/model_aggregation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ def _extract_model_info_from_crd(
model_category_type = model_crd.spec.modelType.value

config = model_crd.spec.modelConfig
if model_crd.spec.protocol:
config = {**config, "protocol": model_crd.spec.protocol}
if model_crd.spec.apiFormat:
config = {**config, "apiFormat": model_crd.spec.apiFormat.value}

# Include type-specific config for non-LLM models
if model_category_type == "video":
Expand Down Expand Up @@ -204,7 +208,11 @@ def _extract_model_info_from_crd(
}

def _is_model_compatible_with_shell(
self, provider: Optional[str], shell_type: str, support_model: List[str]
self,
provider: Optional[str],
shell_type: str,
support_model: List[str],
config: Optional[Dict[str, Any]] = None,
) -> bool:
"""
Check if a model is compatible with the given shell type.
Expand All @@ -221,12 +229,18 @@ def _is_model_compatible_with_shell(
# Agno supports OpenAI, Claude and Gemini models
shell_provider_map = {
"Agno": ["openai", "claude", "gemini"],
"ClaudeCode": ["claude"],
"ClaudeCode": ["claude", "openai"],
}

# If supportModel is specified in shell, use it
if support_model:
return provider in support_model
if provider not in support_model:
return False

if shell_type == "ClaudeCode" and provider == "openai":
return self._is_codex_compatible_model_config(config or {})

if support_model:
return True

# Otherwise, filter by shell's supported providers
supported_providers = shell_provider_map.get(shell_type)
Expand All @@ -239,6 +253,18 @@ def _is_model_compatible_with_shell(
# No filter, allow all
return True

@staticmethod
def _is_codex_compatible_model_config(config: Dict[str, Any]) -> bool:
"""Return whether an OpenAI model config can run through CodeXAgent."""
api_format = str(config.get("apiFormat") or config.get("api_format") or "")
protocol = str(config.get("protocol") or "")
wire_api = str(config.get("wire_api") or "")
return (
api_format.lower() == "responses"
or protocol.lower() == "openai-responses"
or wire_api.lower() == "responses"
)

def _get_shell_support_model(
self, db: Session, shell_name: str, current_user: Optional[User] = None
) -> tuple[List[str], str]:
Expand Down Expand Up @@ -403,7 +429,10 @@ def list_available_models(
info = self._extract_model_info_from_crd(model_data)

if shell_type and not self._is_model_compatible_with_shell(
info["provider"], actual_shell_type, support_model
info["provider"],
actual_shell_type,
support_model,
info.get("config"),
):
continue

Expand Down Expand Up @@ -450,7 +479,10 @@ def list_available_models(
public_model_category_type = model_dict.get("model_category_type", "llm")

if shell_type and not self._is_model_compatible_with_shell(
provider, actual_shell_type, support_model
provider,
actual_shell_type,
support_model,
config,
):
continue

Expand Down
84 changes: 84 additions & 0 deletions backend/tests/api/endpoints/test_adapter_teams_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

from app.api.endpoints.adapter import teams as teams_endpoint


def test_default_team_config_includes_wework_mode(monkeypatch):
monkeypatch.setattr(
teams_endpoint.settings,
"DEFAULT_TEAM_WEWORK",
"wegent-wework-hidden#default",
)
monkeypatch.setattr(
teams_endpoint.settings,
"DEFAULT_TEAM_CHAT",
"wegent-chat#default",
)

config = teams_endpoint._get_default_teams_config()

assert config["wework"] == {
"name": "wegent-wework-hidden",
"namespace": "default",
}
assert config["chat"] == {"name": "wegent-chat", "namespace": "default"}


def test_add_default_for_modes_marks_wework_team():
items = [
{"name": "wegent-wework-hidden", "namespace": "default"},
{"name": "wegent-chat", "namespace": "default"},
]
default_config = {
"wework": {"name": "wegent-wework-hidden", "namespace": "default"},
"chat": {"name": "wegent-chat", "namespace": "default"},
}

result = teams_endpoint._add_default_for_modes(items, default_config)

assert result[0]["default_for_modes"] == ["wework"]
assert result[1]["default_for_modes"] == ["chat"]


def test_list_teams_api_marks_wework_default(monkeypatch):
from types import SimpleNamespace

from fastapi import FastAPI
from fastapi.testclient import TestClient

from app.api.dependencies import get_db
from app.core import security

monkeypatch.setattr(
teams_endpoint.settings,
"DEFAULT_TEAM_WEWORK",
"wegent-wework-hidden#default",
)
monkeypatch.setattr(teams_endpoint.settings, "DEFAULT_TEAM_CHAT", "")
monkeypatch.setattr(teams_endpoint.settings, "DEFAULT_TEAM_CODE", "")
monkeypatch.setattr(teams_endpoint.settings, "DEFAULT_TEAM_KNOWLEDGE", "")
monkeypatch.setattr(teams_endpoint.settings, "DEFAULT_TEAM_TASK", "")
monkeypatch.setattr(
teams_endpoint.team_kinds_service,
"get_user_teams",
lambda **_: [
{
"id": 1,
"name": "wegent-wework-hidden",
"namespace": "default",
"is_active": True,
}
],
)

app = FastAPI()
app.include_router(teams_endpoint.router, prefix="/teams")
app.dependency_overrides[get_db] = lambda: None
app.dependency_overrides[security.get_current_user] = lambda: SimpleNamespace(id=1)

response = TestClient(app).get("/teams?page=1&limit=100")

assert response.status_code == 200
assert response.json()["items"][0]["default_for_modes"] == ["wework"]
27 changes: 27 additions & 0 deletions backend/tests/services/chat/test_reasoning_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,30 @@ def test_execution_request_from_dict_preserves_reasoning_config(self):
}
request = ExecutionRequest.from_dict(data)
assert request.reasoning_config == {"effort": "low", "summary": "concise"}


class TestModelOptionsReasoningConfig:
"""Tests for UI model options reasoning normalization."""

def test_model_options_reasoning_object_preserves_summary(self):
"""Test object-shaped UI reasoning does not become effort text."""
from app.services.chat.trigger.unified import _reasoning_from_model_options

payload = MagicMock()
payload.model_options = {"reasoning": {"summary": "detailed"}}

assert _reasoning_from_model_options(payload) == {"summary": "detailed"}

def test_model_options_reasoning_object_maps_effort_and_summary(self):
"""Test object-shaped UI reasoning maps supported fields separately."""
from app.services.chat.trigger.unified import _reasoning_from_model_options

payload = MagicMock()
payload.model_options = {
"reasoning": {"effort": "high", "summary": "concise"},
}

assert _reasoning_from_model_options(payload) == {
"effort": "high",
"summary": "concise",
}
20 changes: 16 additions & 4 deletions backend/tests/services/test_model_aggregation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,21 @@ def test_get_shell_support_model_not_found(self, test_db: Session, test_user: Us

def test_is_model_compatible_with_shell_claudecode(self, test_db: Session):
"""Test model compatibility for ClaudeCode shell type."""
# ClaudeCode only supports Claude models
# ClaudeCode keeps Claude support and routes OpenAI Responses models to CodeX.
assert model_aggregation_service._is_model_compatible_with_shell(
"claude", "ClaudeCode", []
)
assert model_aggregation_service._is_model_compatible_with_shell(
"openai", "ClaudeCode", [], {"apiFormat": "responses"}
)
assert model_aggregation_service._is_model_compatible_with_shell(
"openai", "ClaudeCode", [], {"protocol": "openai-responses"}
)
assert not model_aggregation_service._is_model_compatible_with_shell(
"openai", "ClaudeCode", []
"openai", "ClaudeCode", [], {"apiFormat": "chat/completions"}
)
assert not model_aggregation_service._is_model_compatible_with_shell(
"openai", "ClaudeCode", ["openai"], {"apiFormat": "chat/completions"}
)
assert not model_aggregation_service._is_model_compatible_with_shell(
"gemini", "ClaudeCode", []
Expand Down Expand Up @@ -218,9 +227,12 @@ def test_custom_shell_inherits_model_filter_from_base(
assert model_aggregation_service._is_model_compatible_with_shell(
"claude", shell_type, support_model
)
# OpenAI should NOT be allowed for ClaudeCode type
# OpenAI should only be allowed when it uses the Responses API for CodeX.
assert not model_aggregation_service._is_model_compatible_with_shell(
"openai", shell_type, support_model
"openai", shell_type, support_model, {"apiFormat": "chat/completions"}
)
assert model_aggregation_service._is_model_compatible_with_shell(
"openai", shell_type, support_model, {"apiFormat": "responses"}
)
# Gemini should NOT be allowed for ClaudeCode type
assert not model_aggregation_service._is_model_compatible_with_shell(
Expand Down
3 changes: 2 additions & 1 deletion docker/executor/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ WORKDIR /app
COPY executor/pyproject.toml /app/executor/pyproject.toml

RUN cd /app/executor && uv pip install --system --no-cache -r pyproject.toml
RUN uv pip install --system --no-cache --no-deps openai-codex==0.1.0b2

# Install PyInstaller for building standalone binary (using uv for speed)
RUN uv pip install --system pyinstaller
Expand All @@ -37,7 +38,7 @@ RUN dnf install -y \
atk at-spi2-atk libXcomposite libXdamage libXfixes libXrandr libxkbcommon \
# Skill-creator dependencies (JSON parsing and packaging)
jq zip \
&& npm install -g @anthropic-ai/claude-code@2.1.33 \
&& npm install -g @anthropic-ai/claude-code@2.1.33 @openai/codex@0.132.0 \
&& dnf clean all \
&& rm -rf /var/cache/dnf/*

Expand Down
Loading
Loading