From 22e129b5145f5b18a20608dfaa14206346e98bf6 Mon Sep 17 00:00:00 2001 From: danfeiyang <1243702693@qq.com> Date: Wed, 25 Feb 2026 01:40:25 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix=EF=BC=9AWorkspace=20path=20in=20onboard?= =?UTF-8?q?=20command=20ignores=20config=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nanobot/cli/commands.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 1c20b50..acea9e2 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -178,8 +178,9 @@ def onboard(): save_config(Config()) console.print(f"[green]✓[/green] Created config at {config_path}") - # Create workspace - workspace = get_workspace_path() + # Create workspace , use config workspace path if exists, otherwise use ~/.nanobot/workspace; try './workspace' will create a workspace + # on the root dir of the project + workspace = get_workspace_path(config.workspace_path) if not workspace.exists(): workspace.mkdir(parents=True, exist_ok=True) From db37ecbfd290e043625a94192ac0d9540c9593a8 Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Tue, 17 Mar 2026 04:28:24 +0000 Subject: [PATCH 2/3] fix(custom): support extraHeaders for OpenAI-compatible endpoints --- nanobot/cli/commands.py | 1 + nanobot/providers/custom_provider.py | 17 +++++++++++++--- tests/test_commands.py | 29 +++++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 659dd94..23b7dfc 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -364,6 +364,7 @@ def _make_provider(config: Config): api_key=p.api_key if p else "no-key", api_base=config.get_api_base(model) or "http://localhost:8000/v1", default_model=model, + extra_headers=p.extra_headers if p else None, ) # Azure OpenAI: direct Azure OpenAI endpoint with deployment name elif provider_name == "azure_openai": diff --git a/nanobot/providers/custom_provider.py b/nanobot/providers/custom_provider.py index f16c69b..e177e55 100644 --- a/nanobot/providers/custom_provider.py +++ b/nanobot/providers/custom_provider.py @@ -13,14 +13,25 @@ from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest class CustomProvider(LLMProvider): - def __init__(self, api_key: str = "no-key", api_base: str = "http://localhost:8000/v1", default_model: str = "default"): + def __init__( + self, + api_key: str = "no-key", + api_base: str = "http://localhost:8000/v1", + default_model: str = "default", + extra_headers: dict[str, str] | None = None, + ): super().__init__(api_key, api_base) self.default_model = default_model - # Keep affinity stable for this provider instance to improve backend cache locality. + # Keep affinity stable for this provider instance to improve backend cache locality, + # while still letting users attach provider-specific headers for custom gateways. + default_headers = { + "x-session-affinity": uuid.uuid4().hex, + **(extra_headers or {}), + } self._client = AsyncOpenAI( api_key=api_key, base_url=api_base, - default_headers={"x-session-affinity": uuid.uuid4().hex}, + default_headers=default_headers, ) async def chat(self, messages: list[dict[str, Any]], tools: list[dict[str, Any]] | None = None, diff --git a/tests/test_commands.py b/tests/test_commands.py index cb77bde..b09c955 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from typer.testing import CliRunner -from nanobot.cli.commands import app +from nanobot.cli.commands import _make_provider, app from nanobot.config.schema import Config from nanobot.providers.litellm_provider import LiteLLMProvider from nanobot.providers.openai_codex_provider import _strip_model_prefix @@ -199,6 +199,33 @@ def test_openai_codex_strip_prefix_supports_hyphen_and_underscore(): assert _strip_model_prefix("openai_codex/gpt-5.1-codex") == "gpt-5.1-codex" +def test_make_provider_passes_extra_headers_to_custom_provider(): + config = Config.model_validate( + { + "agents": {"defaults": {"provider": "custom", "model": "gpt-4o-mini"}}, + "providers": { + "custom": { + "apiKey": "test-key", + "apiBase": "https://example.com/v1", + "extraHeaders": { + "APP-Code": "demo-app", + "x-session-affinity": "sticky-session", + }, + } + }, + } + ) + + with patch("nanobot.providers.custom_provider.AsyncOpenAI") as mock_async_openai: + _make_provider(config) + + kwargs = mock_async_openai.call_args.kwargs + assert kwargs["api_key"] == "test-key" + assert kwargs["base_url"] == "https://example.com/v1" + assert kwargs["default_headers"]["APP-Code"] == "demo-app" + assert kwargs["default_headers"]["x-session-affinity"] == "sticky-session" + + @pytest.fixture def mock_agent_runtime(tmp_path): """Mock agent command dependencies for focused CLI tests.""" From 40a022afd9e5f15db74fb9208c4ec423d799f945 Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Tue, 17 Mar 2026 05:01:34 +0000 Subject: [PATCH 3/3] fix(onboard): use configured workspace path on setup --- nanobot/cli/commands.py | 3 ++- tests/test_commands.py | 12 +++++++----- tests/test_config_migration.py | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 47f7316..a1a6341 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -282,7 +282,8 @@ def onboard(): save_config(config) console.print(f"[green]✓[/green] Config refreshed at {config_path} (existing values preserved)") else: - save_config(Config()) + config = Config() + save_config(config) console.print(f"[green]✓[/green] Created config at {config_path}") console.print("[dim]Config template now uses `maxTokens` + `contextWindowTokens`; `memoryWindow` is no longer a runtime setting.[/dim]") diff --git a/tests/test_commands.py b/tests/test_commands.py index b09c955..ce36a6d 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -45,7 +45,7 @@ def mock_paths(): mock_ws.return_value = workspace_dir mock_sc.side_effect = lambda config: config_file.write_text("{}") - yield config_file, workspace_dir + yield config_file, workspace_dir, mock_ws if base_dir.exists(): shutil.rmtree(base_dir) @@ -53,7 +53,7 @@ def mock_paths(): def test_onboard_fresh_install(mock_paths): """No existing config — should create from scratch.""" - config_file, workspace_dir = mock_paths + config_file, workspace_dir, mock_ws = mock_paths result = runner.invoke(app, ["onboard"]) @@ -64,11 +64,13 @@ def test_onboard_fresh_install(mock_paths): assert config_file.exists() assert (workspace_dir / "AGENTS.md").exists() assert (workspace_dir / "memory" / "MEMORY.md").exists() + expected_workspace = Config().workspace_path + assert mock_ws.call_args.args == (expected_workspace,) def test_onboard_existing_config_refresh(mock_paths): """Config exists, user declines overwrite — should refresh (load-merge-save).""" - config_file, workspace_dir = mock_paths + config_file, workspace_dir, _ = mock_paths config_file.write_text('{"existing": true}') result = runner.invoke(app, ["onboard"], input="n\n") @@ -82,7 +84,7 @@ def test_onboard_existing_config_refresh(mock_paths): def test_onboard_existing_config_overwrite(mock_paths): """Config exists, user confirms overwrite — should reset to defaults.""" - config_file, workspace_dir = mock_paths + config_file, workspace_dir, _ = mock_paths config_file.write_text('{"existing": true}') result = runner.invoke(app, ["onboard"], input="y\n") @@ -95,7 +97,7 @@ def test_onboard_existing_config_overwrite(mock_paths): def test_onboard_existing_workspace_safe_create(mock_paths): """Workspace exists — should not recreate, but still add missing templates.""" - config_file, workspace_dir = mock_paths + config_file, workspace_dir, _ = mock_paths workspace_dir.mkdir(parents=True) config_file.write_text("{}") diff --git a/tests/test_config_migration.py b/tests/test_config_migration.py index f800fb5..2a446b7 100644 --- a/tests/test_config_migration.py +++ b/tests/test_config_migration.py @@ -76,7 +76,7 @@ def test_onboard_refresh_rewrites_legacy_config_template(tmp_path, monkeypatch) ) monkeypatch.setattr("nanobot.config.loader.get_config_path", lambda: config_path) - monkeypatch.setattr("nanobot.cli.commands.get_workspace_path", lambda: workspace) + monkeypatch.setattr("nanobot.cli.commands.get_workspace_path", lambda _workspace=None: workspace) result = runner.invoke(app, ["onboard"], input="n\n") @@ -109,7 +109,7 @@ def test_onboard_refresh_backfills_missing_channel_fields(tmp_path, monkeypatch) ) monkeypatch.setattr("nanobot.config.loader.get_config_path", lambda: config_path) - monkeypatch.setattr("nanobot.cli.commands.get_workspace_path", lambda: workspace) + monkeypatch.setattr("nanobot.cli.commands.get_workspace_path", lambda _workspace=None: workspace) monkeypatch.setattr( "nanobot.channels.registry.discover_all", lambda: {