Skip to content

Commit ed0205e

Browse files
feat(settings): add configurable app identity headers (#132)
* Add configurable app identity headers * Apply suggestion from @frostming * Apply suggestion from @frostming --------- Co-authored-by: Frost Ming <mianghong@gmail.com>
1 parent 212aab8 commit ed0205e

6 files changed

Lines changed: 54 additions & 14 deletions

File tree

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,16 @@ Lines starting with `,` enter internal command mode (`,help`, `,skill name=my-sk
103103

104104
## Configuration
105105

106-
| Variable | Default | Description |
107-
| --------------------------- | ---------------------------------- | ----------------------------------------------- |
108-
| `BUB_MODEL` | `openrouter:qwen/qwen3-coder-next` | Model identifier |
109-
| `BUB_API_KEY` || Provider key (optional with `bub login openai`) |
110-
| `BUB_API_BASE` || Custom provider endpoint |
111-
| `BUB_API_FORMAT` | `completion` | `completion`, `responses`, or `messages` |
112-
| `BUB_MAX_STEPS` | `50` | Max tool-use loop iterations |
113-
| `BUB_MAX_TOKENS` | `1024` | Max tokens per model call |
114-
| `BUB_MODEL_TIMEOUT_SECONDS` || Model call timeout (seconds) |
106+
| Variable | Default | Description |
107+
|----------|---------|-------------|
108+
| `BUB_MODEL` | `openrouter:qwen/qwen3-coder-next` | Model identifier |
109+
| `BUB_API_KEY` || Provider key (optional with `bub login openai`) |
110+
| `BUB_API_BASE` || Custom provider endpoint |
111+
| `BUB_API_FORMAT` | `completion` | `completion`, `responses`, or `messages` |
112+
| `BUB_CLIENT_ARGS` | `{"extra_headers":{"HTTP-Referer":"https://bub.build/","X-Title":"Bub"}}` | JSON object forwarded to the underlying model client |
113+
| `BUB_MAX_STEPS` | `50` | Max tool-use loop iterations |
114+
| `BUB_MAX_TOKENS` | `1024` | Max tokens per model call |
115+
| `BUB_MODEL_TIMEOUT_SECONDS` || Model call timeout (seconds) |
115116

116117
## Background
117118

env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
# - messages: chat-completions-style messages API
3232
# BUB_API_FORMAT=completion
3333

34+
# Optional JSON object forwarded to the underlying model client.
35+
# Default in code includes Bub attribution headers for compatible providers.
36+
# Set to `null` to disable, or provide your own JSON object.
37+
# BUB_CLIENT_ARGS={"extra_headers":{"HTTP-Referer":"https://bub.build/","X-Title":"Bub"}}
38+
3439
# ---------------------------------------------------------------------------
3540
# Channel manager
3641
# ---------------------------------------------------------------------------
@@ -55,3 +60,4 @@
5560
# ---------------------------------------------------------------------------
5661
# BUB_MODEL=openrouter:qwen/qwen3-coder-next
5762
# BUB_API_KEY=sk-or-...
63+
# BUB_CLIENT_ARGS={"extra_headers":{"HTTP-Referer":"https://openclaw.ai","X-Title":"OpenClaw"}}

src/bub/builtin/agent.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
from bub.utils import workspace_from_state
2929

3030
CONTINUE_PROMPT = "Continue the task."
31-
DEFAULT_BUB_HEADERS = {"HTTP-Referer": "https://bub.build/", "X-Title": "Bub"}
3231
HINT_RE = re.compile(r"\$([A-Za-z0-9_.-]+)")
3332

3433

@@ -222,7 +221,6 @@ async def _run_tools_once(
222221
allowed_tools: Collection[str] | None = None,
223222
allowed_skills: Collection[str] | None = None,
224223
) -> ToolAutoResult:
225-
extra_options = {"extra_headers": DEFAULT_BUB_HEADERS} if self.settings.model.startswith("openrouter:") else {}
226224
prompt_text = prompt if isinstance(prompt, str) else _extract_text_from_parts(prompt)
227225
if allowed_tools is not None:
228226
allowed_tools = {name.casefold() for name in allowed_tools}
@@ -240,7 +238,6 @@ async def _run_tools_once(
240238
max_tokens=self.settings.max_tokens,
241239
tools=model_tools(tools),
242240
model=model,
243-
**extra_options,
244241
)
245242

246243
def _system_prompt(self, prompt: str, state: State, allowed_skills: set[str] | None = None) -> str:
@@ -284,6 +281,7 @@ def _build_llm(settings: AgentSettings, tape_store: AsyncTapeStore, tape_context
284281
fallback_models=settings.fallback_models,
285282
api_key_resolver=openai_codex_oauth_resolver(),
286283
tape_store=tape_store,
284+
client_args=settings.client_args,
287285
api_format=settings.api_format,
288286
context=tape_context,
289287
verbose=settings.verbose,

src/bub/builtin/settings.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import re
66
from collections.abc import Callable
77
from functools import lru_cache
8-
from typing import Literal
8+
from typing import Any, Literal
99

1010
from pydantic import Field
1111
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, YamlConfigSettingsSource
@@ -32,6 +32,10 @@ def default_factory() -> dict[str, str] | None:
3232
return default_factory
3333

3434

35+
def default_client_args() -> dict[str, Any]:
36+
return {"extra_headers": {"HTTP-Referer": "https://bub.build/", "X-Title": "Bub"}}
37+
38+
3539
class AgentSettings(BaseSettings):
3640
"""Configuration settings for the Agent."""
3741

@@ -45,6 +49,7 @@ class AgentSettings(BaseSettings):
4549
max_steps: int = 50
4650
max_tokens: int = DEFAULT_MAX_TOKENS
4751
model_timeout_seconds: int | None = None
52+
client_args: dict[str, Any] | None = Field(default_factory=default_client_args)
4853
verbose: int = Field(default=0, description="Verbosity level for logging. Higher means more verbose.", ge=0, le=2)
4954

5055
@classmethod

tests/test_builtin_agent.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,22 @@ def __init__(self, *args: object, **kwargs: object) -> None:
2626
monkeypatch.setattr(agent_module, "LLM", FakeLLM)
2727
monkeypatch.setattr(openai_codex, "openai_codex_oauth_resolver", lambda: resolver)
2828

29-
settings = AgentSettings(model="openai:gpt-5-codex", api_key=None, api_base=None)
29+
settings = AgentSettings(
30+
model="openai:gpt-5-codex",
31+
api_key=None,
32+
api_base=None,
33+
client_args={"extra_headers": {"HTTP-Referer": "https://openclaw.ai", "X-Title": "OpenClaw"}},
34+
)
3035
tape_store = object()
3136

3237
agent_module._build_llm(settings, tape_store, "ctx")
3338

3439
assert captured["args"] == ("openai:gpt-5-codex",)
3540
assert captured["kwargs"]["api_key"] is None
3641
assert captured["kwargs"]["api_base"] is None
42+
assert captured["kwargs"]["client_args"] == {
43+
"extra_headers": {"HTTP-Referer": "https://openclaw.ai", "X-Title": "OpenClaw"},
44+
}
3745
assert captured["kwargs"]["api_key_resolver"] is resolver
3846
assert captured["kwargs"]["tape_store"] is tape_store
3947
assert captured["kwargs"]["context"] == "ctx"

tests/test_settings.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def test_settings_no_keys_returns_none() -> None:
4242

4343
assert settings.api_key is None
4444
assert settings.api_base is None
45+
assert settings.client_args == {"extra_headers": {"HTTP-Referer": "https://bub.build/", "X-Title": "Bub"}}
4546

4647

4748
def test_settings_provider_names_are_lowercased() -> None:
@@ -74,6 +75,10 @@ def test_settings_load_values_from_yaml(tmp_path: Path) -> None:
7475
openai: sk-yaml
7576
api_base:
7677
openai: https://api.openai.com
78+
client_args:
79+
extra_headers:
80+
HTTP-Referer: https://openclaw.ai
81+
X-Title: OpenClaw
7782
""".strip(),
7883
)
7984

@@ -85,6 +90,9 @@ def test_settings_load_values_from_yaml(tmp_path: Path) -> None:
8590
assert settings.max_steps == 77
8691
assert settings.api_key == {"openai": "sk-yaml"}
8792
assert settings.api_base == {"openai": "https://api.openai.com"}
93+
assert settings.client_args == {
94+
"extra_headers": {"HTTP-Referer": "https://openclaw.ai", "X-Title": "OpenClaw"},
95+
}
8896

8997

9098
def test_env_settings_override_yaml(tmp_path: Path) -> None:
@@ -94,6 +102,10 @@ def test_env_settings_override_yaml(tmp_path: Path) -> None:
94102
model: openai:gpt-5
95103
api_key: sk-yaml
96104
max_steps: 77
105+
client_args:
106+
extra_headers:
107+
HTTP-Referer: https://yaml.example
108+
X-Title: YAML App
97109
""".strip(),
98110
)
99111

@@ -103,6 +115,7 @@ def test_env_settings_override_yaml(tmp_path: Path) -> None:
103115
"BUB_HOME": str(tmp_path),
104116
"BUB_MODEL": "anthropic:claude-3-7-sonnet",
105117
"BUB_API_KEY": "sk-env",
118+
"BUB_CLIENT_ARGS": '{"extra_headers":{"HTTP-Referer":"https://env.example","X-Title":"Env App"}}',
106119
"BUB_MAX_STEPS": "12",
107120
},
108121
clear=True,
@@ -112,6 +125,15 @@ def test_env_settings_override_yaml(tmp_path: Path) -> None:
112125
assert settings.model == "anthropic:claude-3-7-sonnet"
113126
assert settings.api_key == "sk-env"
114127
assert settings.max_steps == 12
128+
assert settings.client_args == {
129+
"extra_headers": {"HTTP-Referer": "https://env.example", "X-Title": "Env App"},
130+
}
131+
132+
133+
def test_settings_client_args_can_be_disabled() -> None:
134+
settings = _settings_with_env({"BUB_CLIENT_ARGS": "null"})
135+
136+
assert settings.client_args is None
115137

116138

117139
def test_load_settings_reads_yaml_from_bub_home(tmp_path: Path) -> None:

0 commit comments

Comments
 (0)