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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
16 changes: 9 additions & 7 deletions src/bub/builtin/model_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/bub/builtin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]:
Expand Down
33 changes: 33 additions & 0 deletions tests/test_builtin_model_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
9 changes: 8 additions & 1 deletion tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(),
)

Expand All @@ -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:
Expand All @@ -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,
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions website/src/content/docs/docs/reference/settings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```

Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions website/src/content/docs/zh-cn/docs/reference/settings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```

Expand All @@ -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 收集。
Expand Down
Loading