diff --git a/README.md b/README.md index 64463d41..f901a45c 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ Lines starting with `,` enter internal command mode (`,help`, `,skill name=my-sk | `BUB_API_KEY` | — | Provider key (optional with `bub login openai`) | | `BUB_API_BASE` | — | Custom provider endpoint | | `BUB_CLIENT_ARGS` | — | JSON object forwarded to the underlying model client | +| `BUB_COMPLETION_ARGS` | — | JSON object forwarded to each completion call | | `BUB_MAX_STEPS` | `50` | Max tool-use loop iterations | | `BUB_MAX_TOKENS` | `16384` | Max tokens per model call | | `BUB_MODEL_TIMEOUT_SECONDS` | — | Model call timeout (seconds) | diff --git a/src/bub/builtin/model_runner.py b/src/bub/builtin/model_runner.py index bd7365a6..fbbb4a71 100644 --- a/src/bub/builtin/model_runner.py +++ b/src/bub/builtin/model_runner.py @@ -92,14 +92,16 @@ async def completion_response( for index, (candidate, llm) in enumerate(clients): try: streaming = llm.SUPPORTS_COMPLETION_STREAMING - return await llm.acompletion( - model=candidate.model_id, - messages=completion_messages, - tools=tool_payloads, - max_tokens=max_tokens if max_tokens is not None else self.settings.max_tokens, - stream=streaming, + completion_kwargs = { + **self.settings.completion_args, **_extra_options(llm, stream=streaming), - ) + "model": candidate.model_id, + "messages": completion_messages, + "tools": tool_payloads, + "max_tokens": max_tokens if max_tokens is not None else self.settings.max_tokens, + "stream": streaming, + } + return cast("CompletionResult", await llm.acompletion(**completion_kwargs)) except Exception as exc: if completion_error is None: completion_error = exc diff --git a/src/bub/builtin/settings.py b/src/bub/builtin/settings.py index 49d88389..b440d864 100644 --- a/src/bub/builtin/settings.py +++ b/src/bub/builtin/settings.py @@ -60,6 +60,7 @@ class AgentSettings(Settings): max_tokens: int = DEFAULT_MAX_TOKENS model_timeout_seconds: int | None = None client_args: dict[str, Any] = Field(default_factory=dict) + completion_args: dict[str, Any] = Field(default_factory=dict) verbose: int = Field(default=0, description="Verbosity level for logging. Higher means more verbose.", ge=0, le=2) @classmethod @@ -79,9 +80,9 @@ def settings_customise_sources( file_secret_settings, ) - @field_validator("client_args", mode="before") + @field_validator("client_args", "completion_args", mode="before") @classmethod - def default_client_args(cls, value: Any) -> Any: + def default_dict_args(cls, value: Any) -> Any: return {} if value is None else value def model_candidates(self, model: str) -> list[ModelCandidate]: diff --git a/tests/test_builtin_model_runner.py b/tests/test_builtin_model_runner.py index f60e4543..4dc1b4db 100644 --- a/tests/test_builtin_model_runner.py +++ b/tests/test_builtin_model_runner.py @@ -136,3 +136,36 @@ async def test_anthropic_prompt_caching_is_requested() -> None: assert llm.completion_kwargs["stream"] is True assert llm.completion_kwargs["cache_control"] == {"type": "ephemeral"} assert "stream_options" not in llm.completion_kwargs + + +@pytest.mark.asyncio +async def test_completion_args_are_forwarded_without_overriding_managed_args() -> None: + llm = _FakeStreamingOpenAIProvider() + runner = _FakeOpenAIModelRunner( + AgentSettings.model_construct( + model="openai:gpt-test", + max_tokens=100, + completion_args={ + "reasoning_effort": "high", + "model": "ignored-model", + "max_tokens": 1, + "stream": False, + "stream_options": {"include_usage": False}, + }, + ), + llm, + ) + + await runner.completion_response( + model="gpt-test", + messages=[{"role": "user", "content": "hello"}], + tools=[], + max_tokens=42, + ) + + assert llm.completion_kwargs is not None + assert llm.completion_kwargs["reasoning_effort"] == "high" + assert llm.completion_kwargs["model"] == "gpt-test" + assert llm.completion_kwargs["max_tokens"] == 42 + assert llm.completion_kwargs["stream"] is True + assert llm.completion_kwargs["stream_options"] == {"include_usage": True} diff --git a/tests/test_settings.py b/tests/test_settings.py index 6d3c6f63..02127a43 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -38,6 +38,7 @@ def test_settings_no_keys_return_none() -> None: assert settings.api_key is None assert settings.api_base is None assert settings.client_args == {} + assert settings.completion_args == {} def test_settings_provider_names_are_lowercased() -> None: @@ -74,6 +75,8 @@ def test_settings_load_values_from_yaml(load_config) -> None: extra_headers: HTTP-Referer: https://openclaw.ai X-Title: OpenClaw +completion_args: + reasoning_effort: high """.strip(), ) @@ -87,6 +90,7 @@ def test_settings_load_values_from_yaml(load_config) -> None: assert settings.client_args == { "extra_headers": {"HTTP-Referer": "https://openclaw.ai", "X-Title": "OpenClaw"}, } + assert settings.completion_args == {"reasoning_effort": "high"} def test_env_settings_override_yaml(load_config) -> None: @@ -106,6 +110,7 @@ def test_env_settings_override_yaml(load_config) -> None: "BUB_MODEL": "anthropic:claude-3-7-sonnet", "BUB_API_KEY": "sk-env", "BUB_CLIENT_ARGS": '{"extra_headers":{"HTTP-Referer":"https://env.example","X-Title":"Env App"}}', + "BUB_COMPLETION_ARGS": '{"reasoning_effort":"medium"}', "BUB_MAX_STEPS": "12", }, clear=True, @@ -119,12 +124,14 @@ def test_env_settings_override_yaml(load_config) -> None: assert settings.client_args == { "extra_headers": {"HTTP-Referer": "https://env.example", "X-Title": "Env App"}, } + assert settings.completion_args == {"reasoning_effort": "medium"} def test_settings_client_args_can_be_disabled() -> None: - settings = _settings_with_env({"BUB_CLIENT_ARGS": "null"}) + settings = _settings_with_env({"BUB_CLIENT_ARGS": "null", "BUB_COMPLETION_ARGS": "null"}) assert settings.client_args == {} + assert settings.completion_args == {} def test_load_settings_returns_defaults_without_loaded_config() -> None: diff --git a/website/src/content/docs/docs/reference/settings.mdx b/website/src/content/docs/docs/reference/settings.mdx index cd5c9ef7..ce6a471c 100644 --- a/website/src/content/docs/docs/reference/settings.mdx +++ b/website/src/content/docs/docs/reference/settings.mdx @@ -48,6 +48,7 @@ class AgentSettings(Settings): max_tokens: int = DEFAULT_MAX_TOKENS # 16384 model_timeout_seconds: int | None = None client_args: dict[str, Any] = Field(default_factory=dict) + completion_args: dict[str, Any] = Field(default_factory=dict) verbose: int = Field(default=0, ge=0, le=2) ``` @@ -65,6 +66,7 @@ Loaded under the YAML root section. | `BUB_MAX_TOKENS` | `16384` | `max_tokens` | Maximum tokens per model call. | | `BUB_MODEL_TIMEOUT_SECONDS` | `null` | `model_timeout_seconds` | Per-call timeout in seconds. | | `BUB_CLIENT_ARGS` | `{}` | `client_args` | Extra kwargs passed to the underlying model client (JSON / dict). | +| `BUB_COMPLETION_ARGS` | `{}` | `completion_args` | Extra kwargs passed to each completion call, e.g. `{"reasoning_effort":"high"}`. Bub-managed arguments take precedence. | | `BUB_VERBOSE` | `0` | `verbose` | Logging verbosity level (`0`–`2`). | Provider-specific defaults are gathered at startup by scanning `os.environ` for `^BUB_(.+)_(API_KEY|API_BASE)$` and lowercasing the captured provider name. diff --git a/website/src/content/docs/zh-cn/docs/reference/settings.mdx b/website/src/content/docs/zh-cn/docs/reference/settings.mdx index bef7297a..674a7a3b 100644 --- a/website/src/content/docs/zh-cn/docs/reference/settings.mdx +++ b/website/src/content/docs/zh-cn/docs/reference/settings.mdx @@ -48,6 +48,7 @@ class AgentSettings(Settings): max_tokens: int = DEFAULT_MAX_TOKENS # 16384 model_timeout_seconds: int | None = None client_args: dict[str, Any] = Field(default_factory=dict) + completion_args: dict[str, Any] = Field(default_factory=dict) verbose: int = Field(default=0, ge=0, le=2) ``` @@ -65,6 +66,7 @@ class AgentSettings(Settings): | `BUB_MAX_TOKENS` | `16384` | `max_tokens` | 单次模型调用的最大 token 数。 | | `BUB_MODEL_TIMEOUT_SECONDS` | `null` | `model_timeout_seconds` | 单次调用的超时秒数。 | | `BUB_CLIENT_ARGS` | `{}` | `client_args` | 传递给底层模型 client 的额外 kwargs(JSON / dict)。 | +| `BUB_COMPLETION_ARGS` | `{}` | `completion_args` | 传递给每次 completion 调用的额外 kwargs,例如 `{"reasoning_effort":"high"}`;Bub 管理的参数优先。 | | `BUB_VERBOSE` | `0` | `verbose` | 日志详细级别(`0`–`2`)。 | 启动时 `ProviderSpecificEnvSource` 会扫描 `os.environ` 中匹配 `^BUB_(.+)_(API_KEY|API_BASE)$` 的变量,把捕获到的 provider 名称小写后作为 key 收集。