Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 14 additions & 0 deletions docs/en/user-guide/ai-devices/local-device-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ curl -fL -o wegent-executor \
chmod +x wegent-executor
```

#### Use Device-Local CLI Configuration

By default, the executor uses the Claude/Codex model and provider configuration issued by Wegent. If a device already has local Codex CLI login or provider configuration, set this environment variable to let Codex use the device-local configuration:

```bash
WEGENT_LOCAL_CLI_CONFIG_RUNTIMES=codex
```

When the variable is empty or unset, all runtimes keep using the system-issued configuration. The currently supported value is:

- `codex`: Codex uses device-local configuration, and Wegent no longer injects the Codex provider.

This setting only affects the current device, which is useful when different devices should mix system-issued configuration and local Codex CLI configuration differently. Claude keeps using Wegent-issued configuration.

### Building a Device Image

The repository provides `docker/device/Dockerfile` for cloud device or local device base images. The image installs `code-server`, the `weiboplat.wecoder-agent` extension, Claude Code CLI, `ttyd`, Node.js 22, Python, Git, and copies `executor/dist/wegent-executor` to `/app/executor` and `~/.wegent-executor/bin/wegent-executor`.
Expand Down
14 changes: 14 additions & 0 deletions docs/zh/user-guide/ai-devices/local-device-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ curl -fL -o wegent-executor \
chmod +x wegent-executor
```

#### 使用设备本机 CLI 配置

默认情况下,executor 会使用 Wegent 下发的 Claude/Codex 模型和 provider 配置。如果某台设备已经在本机配置好了 Codex CLI 登录或 provider,可以通过环境变量让 Codex 使用本机配置:

```bash
WEGENT_LOCAL_CLI_CONFIG_RUNTIMES=codex
```

该变量为空或未设置时,所有 runtime 都继续使用系统下发配置。当前仅支持以下值:

- `codex`:Codex 使用设备本机配置,不再注入 Wegent Codex provider。

该配置只影响当前设备,适合不同设备分别使用系统下发配置或本机 Codex CLI 配置的场景。Claude 仍保持 Wegent 下发的配置行为。

### 构建设备镜像

仓库提供 `docker/device/Dockerfile` 用于构建云设备或本地设备基础镜像。该镜像会安装 `code-server`、`weiboplat.wecoder-agent` 扩展、Claude Code CLI、`ttyd`、Node.js 22、Python、Git,并把 `executor/dist/wegent-executor` 放到 `/app/executor` 和 `~/.wegent-executor/bin/wegent-executor`。
Expand Down
43 changes: 20 additions & 23 deletions executor/agents/codex/codex_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,36 +200,23 @@ async def _start_codex_client(self) -> None:
await self._codex.__aenter__()

async def _open_thread(self) -> None:
from openai_codex import ApprovalMode

assert self.codex_config is not None
thread_id = self._session_store.load(
self.task_id,
self._bot_id,
self.new_session,
)
developer_instructions = self._build_developer_instructions()
thread_kwargs = self._build_thread_kwargs(developer_instructions)
try:
if thread_id:
self._thread = await self._codex.thread_resume(
thread_id,
approval_mode=ApprovalMode.deny_all,
config=self.codex_config.thread_config,
cwd=self.project_path,
developer_instructions=developer_instructions,
model=self.codex_config.model,
model_provider=self.codex_config.model_provider,
sandbox=self._sandbox_full_access(),
**thread_kwargs,
)
else:
self._thread = await self._codex.thread_start(
approval_mode=ApprovalMode.deny_all,
config=self.codex_config.thread_config,
cwd=self.project_path,
developer_instructions=developer_instructions,
model=self.codex_config.model,
model_provider=self.codex_config.model_provider,
sandbox=self._sandbox_full_access(),
**thread_kwargs,
service_name="wegent",
)
except Exception:
Expand All @@ -239,17 +226,27 @@ async def _open_thread(self) -> None:
"Failed to resume Codex thread %s, starting a new one", thread_id
)
self._thread = await self._codex.thread_start(
approval_mode=ApprovalMode.deny_all,
config=self.codex_config.thread_config,
cwd=self.project_path,
developer_instructions=developer_instructions,
model=self.codex_config.model,
model_provider=self.codex_config.model_provider,
sandbox=self._sandbox_full_access(),
**thread_kwargs,
service_name="wegent",
)
self._session_store.save(self.task_id, self._bot_id, self._thread.id)

def _build_thread_kwargs(self, developer_instructions: str) -> dict[str, Any]:
from openai_codex import ApprovalMode

assert self.codex_config is not None
kwargs = {
"approval_mode": ApprovalMode.deny_all,
"config": self.codex_config.thread_config,
"cwd": self.project_path,
"developer_instructions": developer_instructions,
"model": self.codex_config.model,
"sandbox": self._sandbox_full_access(),
}
if self.codex_config.model_provider:
kwargs["model_provider"] = self.codex_config.model_provider
return kwargs

async def _notify_client_created(self) -> None:
if not self.on_client_created_callback:
return
Expand Down
35 changes: 26 additions & 9 deletions executor/agents/codex/config_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from executor.agents.env_value import resolve_env_value
from executor.config import config
from executor.config.local_cli_config import use_local_cli_config
from shared.logger import setup_logger

OPENAI_RESPONSES_PROTOCOL = "openai-responses"
Expand All @@ -30,7 +31,7 @@ class CodeXConfig:

codex_bin: str
model: str
model_provider: str
model_provider: Optional[str]
config_overrides: tuple[str, ...]
thread_config: dict[str, Any]
effort: Optional[str]
Expand All @@ -57,6 +58,20 @@ def build_codex_config(model_config: dict[str, Any]) -> CodeXConfig:
if not model:
raise ValueError("CodeXAgent requires model_config.model_id")

local_config = use_local_cli_config("codex")
reasoning = _normalize_reasoning(model_config.get("reasoning"))
if local_config:
overrides = [f"model={model}"]
return CodeXConfig(
codex_bin=_resolve_codex_binary(config.CODEX_BINARY_PATH),
model=model,
model_provider=None,
config_overrides=tuple(overrides),
thread_config=_build_thread_config(reasoning),
effort=reasoning.get("effort"),
summary=reasoning.get("summary"),
)

base_url = str(model_config.get("base_url") or "").strip()
if not base_url:
raise ValueError("CodeXAgent requires model_config.base_url")
Expand All @@ -69,7 +84,6 @@ def build_codex_config(model_config: dict[str, Any]) -> CodeXConfig:

model_provider = _resolve_model_provider(model_config)
wire_api = _resolve_wire_api(model_config)
reasoning = _normalize_reasoning(model_config.get("reasoning"))

overrides = [
"forced_login_method=api",
Expand All @@ -81,23 +95,26 @@ def build_codex_config(model_config: dict[str, Any]) -> CodeXConfig:
f"model_providers.{model_provider}.experimental_bearer_token={api_key}",
]

thread_config: dict[str, Any] = {}
if reasoning.get("effort"):
thread_config["model_reasoning_effort"] = reasoning["effort"]
if reasoning.get("summary"):
thread_config["model_reasoning_summary"] = reasoning["summary"]

return CodeXConfig(
codex_bin=_resolve_codex_binary(config.CODEX_BINARY_PATH),
model=model,
model_provider=model_provider,
config_overrides=tuple(overrides),
thread_config=thread_config,
thread_config=_build_thread_config(reasoning),
effort=reasoning.get("effort"),
summary=reasoning.get("summary"),
)


def _build_thread_config(reasoning: dict[str, str]) -> dict[str, Any]:
thread_config: dict[str, Any] = {}
if reasoning.get("effort"):
thread_config["model_reasoning_effort"] = reasoning["effort"]
if reasoning.get("summary"):
thread_config["model_reasoning_summary"] = reasoning["summary"]
return thread_config


def _resolve_model_provider(model_config: dict[str, Any]) -> str:
provider = (
model_config.get("codex_model_provider")
Expand Down
24 changes: 24 additions & 0 deletions executor/config/local_cli_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

"""Local CLI runtime configuration helpers."""

import os


LOCAL_CLI_CONFIG_RUNTIMES_ENV = "WEGENT_LOCAL_CLI_CONFIG_RUNTIMES"
SUPPORTED_LOCAL_CLI_CONFIG_RUNTIMES = {"codex"}


def use_local_cli_config(runtime: str) -> bool:
"""Return whether a CLI runtime should use device-local provider config."""
runtime_name = runtime.strip().lower()
if runtime_name not in SUPPORTED_LOCAL_CLI_CONFIG_RUNTIMES:
return False

raw_value = os.getenv(LOCAL_CLI_CONFIG_RUNTIMES_ENV, "")
enabled_runtimes = {
item.strip().lower() for item in raw_value.split(",") if item.strip()
}
return runtime_name in enabled_runtimes
28 changes: 28 additions & 0 deletions executor/tests/agents/test_codex_config_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@
#
# SPDX-License-Identifier: Apache-2.0

import pytest

from executor.agents.codex.config_builder import (
build_codex_config,
is_codex_compatible_model,
)


@pytest.fixture(autouse=True)
def clear_local_cli_config(monkeypatch):
monkeypatch.delenv("WEGENT_LOCAL_CLI_CONFIG_RUNTIMES", raising=False)


def test_is_codex_compatible_model_requires_openai_responses():
assert is_codex_compatible_model({"model": "openai", "api_format": "responses"})
assert is_codex_compatible_model({"model": "openai", "apiFormat": "responses"})
Expand Down Expand Up @@ -55,6 +62,27 @@ def test_build_codex_config_maps_provider_and_reasoning():
assert config.summary == "concise"


def test_build_codex_config_uses_local_cli_config(monkeypatch):
monkeypatch.setenv("WEGENT_LOCAL_CLI_CONFIG_RUNTIMES", "codex")

config = build_codex_config(
{
"model": "openai",
"model_id": "gpt-5.5",
"api_format": "responses",
"reasoning": {"effort": "high", "summary": "concise"},
}
)

assert config.model == "gpt-5.5"
assert config.model_provider is None
assert config.config_overrides == ("model=gpt-5.5",)
assert config.thread_config == {
"model_reasoning_effort": "high",
"model_reasoning_summary": "concise",
}


def test_build_codex_config_resolves_api_key_env_placeholder(monkeypatch):
monkeypatch.setenv("WECODE_USER_API_KEY", "sk-from-executor-env")

Expand Down
20 changes: 20 additions & 0 deletions executor/tests/config/test_local_cli_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

from executor.config.local_cli_config import use_local_cli_config


def test_use_local_cli_config_defaults_to_system(monkeypatch):
monkeypatch.delenv("WEGENT_LOCAL_CLI_CONFIG_RUNTIMES", raising=False)

assert not use_local_cli_config("codex")
assert not use_local_cli_config("claude")


def test_use_local_cli_config_matches_comma_separated_runtime_names(monkeypatch):
monkeypatch.setenv("WEGENT_LOCAL_CLI_CONFIG_RUNTIMES", " codex, Claude ")

assert use_local_cli_config("codex")
assert not use_local_cli_config("claude")
assert not use_local_cli_config("agno")
Loading