feat(pi): support egress injected api keys
lint / lint (push) Successful in 1m38s
test / unit (pull_request) Successful in 31s
test / integration (pull_request) Successful in 17s

This commit is contained in:
2026-06-09 05:56:39 -04:00
parent 5ea9fda69b
commit c8b5ba3812
5 changed files with 112 additions and 19 deletions
+27 -10
View File
@@ -32,7 +32,7 @@ if TYPE_CHECKING:
_DEFAULT_BASE_URL = "http://ollama:11434/v1"
_DEFAULT_MODEL = "qwen2.5-coder:7b"
_PROVIDER_NAME = "ollama"
_DEFAULT_PROVIDER_NAME = "ollama"
def _skills_dir(guest_home: str) -> str:
@@ -56,10 +56,16 @@ def _settings_value(
return default if value is None else value
def _pi_models_json(settings: dict[str, object]) -> tuple[dict[str, object], str]:
def _pi_models_json(
settings: dict[str, object],
) -> tuple[dict[str, object], str, str]:
provider_name = str(
_settings_value(settings, "provider", _DEFAULT_PROVIDER_NAME)
)
base_url = str(_settings_value(settings, "base_url", _DEFAULT_BASE_URL))
api = str(_settings_value(settings, "api", "openai-completions"))
api_key = str(_settings_value(settings, "api_key", "ollama"))
api_key = settings.get("api_key")
api_key_env = str(settings.get("api_key_env", ""))
models_raw = _settings_value(settings, "models", [_DEFAULT_MODEL])
models = [str(model) for model in models_raw] # type: ignore[union-attr]
supports_developer_role = bool(
@@ -68,21 +74,27 @@ def _pi_models_json(settings: dict[str, object]) -> tuple[dict[str, object], str
supports_reasoning_effort = bool(
_settings_value(settings, "supports_reasoning_effort", False)
)
payload: dict[str, object] = {
"providers": {
_PROVIDER_NAME: {
provider: dict[str, object] = {
"baseUrl": base_url,
"api": api,
"apiKey": api_key,
"compat": {
"supportsDeveloperRole": supports_developer_role,
"supportsReasoningEffort": supports_reasoning_effort,
},
"models": [{"id": model} for model in models],
}
if api_key is not None:
provider["apiKey"] = str(api_key)
elif api_key_env:
provider["apiKey"] = "egress-placeholder"
elif provider_name == _DEFAULT_PROVIDER_NAME:
provider["apiKey"] = "ollama"
payload: dict[str, object] = {
"providers": {
provider_name: provider,
}
}
return payload, base_url
return payload, base_url, api_key_env
def _route_host(base_url: str) -> str:
@@ -133,12 +145,13 @@ class PiAgentProvider(AgentProvider):
guest_home = self.guest_home
settings = dict(provider_settings or {})
models_payload, base_url = _pi_models_json(settings)
models_payload, base_url, api_key_env = _pi_models_json(settings)
models_file = state_dir / "pi-models.json"
models_file.write_text(json.dumps(models_payload, indent=2) + "\n")
models_file.chmod(0o600)
has_prompt = prompt_file.exists() and bool(prompt_file.read_text())
auth_scheme = "Bearer" if api_key_env else ""
return AgentProvisionPlan(
template=_RUNTIME.template,
command=_RUNTIME.command,
@@ -152,7 +165,11 @@ class PiAgentProvider(AgentProvider):
has_prompt=has_prompt,
dirs=(AgentProvisionDir(f"{guest_home}/.pi/agent"),),
files=(AgentProvisionFile(models_file, _models_path(guest_home)),),
egress_routes=(EgressRoute(host=_route_host(base_url)),),
egress_routes=(EgressRoute(
host=_route_host(base_url),
auth_scheme=auth_scheme,
token_ref=api_key_env,
),),
)
def provision_skills(self, plan: "BottlePlan", bottle: "Bottle") -> None:
+8 -1
View File
@@ -206,9 +206,11 @@ def _parse_provider_settings(
)
settings = as_json_object(raw, f"bottle '{bottle_name}' agent_provider.settings")
allowed = {
"provider",
"base_url",
"api",
"api_key",
"api_key_env",
"models",
"supports_developer_role",
"supports_reasoning_effort",
@@ -219,13 +221,18 @@ def _parse_provider_settings(
f"bottle '{bottle_name}' agent_provider.settings has unknown "
f"key {key!r}; allowed: {', '.join(sorted(allowed))}"
)
for key in ("base_url", "api", "api_key"):
for key in ("provider", "base_url", "api", "api_key", "api_key_env"):
value = settings.get(key)
if value is not None and (not isinstance(value, str) or not value):
raise ManifestError(
f"bottle '{bottle_name}' agent_provider.settings.{key} must "
"be a non-empty string"
)
if settings.get("api_key") is not None and settings.get("api_key_env") is not None:
raise ManifestError(
f"bottle '{bottle_name}' agent_provider.settings may set either "
"api_key or api_key_env, not both"
)
models = settings.get("models")
if models is not None:
if not isinstance(models, list) or not models:
+14 -2
View File
@@ -13,8 +13,9 @@ provider settings file that targets an unauthenticated Ollama-compatible server.
The default settings assume an Ollama server at `http://ollama:11434/v1`, using
the `openai-completions` API with a dummy API key because Ollama ignores it.
Users can override the base URL, model list, API key, API type, and compatibility
flags through a new `agent_provider.settings` object.
Users can override the provider id, base URL, model list, API key, API-key env
reference, API type, and compatibility flags through a new
`agent_provider.settings` object.
## Problem
@@ -60,8 +61,10 @@ supported for built-in `pi`.
Supported keys:
- `base_url`: string, defaults to `http://ollama:11434/v1`
- `provider`: string, defaults to `ollama`
- `api`: string, defaults to `openai-completions`
- `api_key`: string, defaults to `ollama`
- `api_key_env`: string, optional host env var name for egress auth injection
- `models`: non-empty array of strings, defaults to `["qwen2.5-coder:7b"]`
- `supports_developer_role`: boolean, defaults to `false`
- `supports_reasoning_effort`: boolean, defaults to `false`
@@ -70,6 +73,15 @@ The snake-case manifest keys are converted into Pi's JSON field names:
`baseUrl`, `apiKey`, `supportsDeveloperRole`, and
`supportsReasoningEffort`.
`api_key` and `api_key_env` are mutually exclusive. When targeting a hosted
provider through bot-bottle's egress sidecar, omit `api_key` and set
`api_key_env` to the host env var that holds the API key. The generated
`models.json` receives only an `egress-placeholder` API key, and the egress
route injects the real `Authorization` header from the sidecar env. For example,
OpenRouter can use provider id `openrouter` with
`api_key_env: OPENROUTER_API_KEY`, keeping the key out of the agent env and
`models.json`.
### Provider
`PiAgentProvider.provision_plan` writes `models.json` into the per-launch state
+32
View File
@@ -322,6 +322,38 @@ class TestAgentProviderRuntime(unittest.TestCase):
self.assertTrue(provider["compat"]["supportsDeveloperRole"])
self.assertTrue(provider["compat"]["supportsReasoningEffort"])
def test_pi_plan_can_target_openrouter_with_egress_injected_api_key(self):
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
plan = build_agent_provision_plan(
template="pi",
dockerfile="",
state_dir=Path(tmp),
instance_name="bot-bottle-test",
prompt_file=Path(tmp) / "prompt.txt",
provider_settings={
"provider": "openrouter",
"base_url": "https://openrouter.ai/api/v1",
"api": "openai-completions",
"api_key_env": "OPENROUTER_API_KEY",
"models": ["google/gemma-4-26b-a4b-it:free"],
"supports_reasoning_effort": True,
},
)
models = json.loads(Path(tmp, "pi-models.json").read_text())
provider = models["providers"]["openrouter"]
self.assertEqual("https://openrouter.ai/api/v1", provider["baseUrl"])
self.assertEqual("openai-completions", provider["api"])
self.assertEqual("egress-placeholder", provider["apiKey"])
self.assertEqual(
[{"id": "google/gemma-4-26b-a4b-it:free"}],
provider["models"],
)
self.assertEqual("openrouter.ai", plan.egress_routes[0].host)
self.assertEqual("Bearer", plan.egress_routes[0].auth_scheme)
self.assertEqual("OPENROUTER_API_KEY", plan.egress_routes[0].token_ref)
self.assertNotIn("OPENROUTER_API_KEY", plan.guest_env)
self.assertTrue(provider["compat"]["supportsReasoningEffort"])
def test_pi_prompt_mode_uses_print_flag(self):
self.assertEqual(
["-p", "Read and follow the instructions in /home/node/.bot-bottle-prompt.txt."],
+25
View File
@@ -115,6 +115,7 @@ class TestAgentProviderHostCredentials(unittest.TestCase):
b = _provider_config_bottle({
"template": "pi",
"settings": {
"provider": "ollama",
"base_url": "http://ollama:11434/v1",
"api": "openai-completions",
"api_key": "ollama",
@@ -125,6 +126,7 @@ class TestAgentProviderHostCredentials(unittest.TestCase):
})
self.assertEqual(
{
"provider": "ollama",
"base_url": "http://ollama:11434/v1",
"api": "openai-completions",
"api_key": "ollama",
@@ -135,6 +137,29 @@ class TestAgentProviderHostCredentials(unittest.TestCase):
b.agent_provider.settings,
)
def test_settings_allowed_for_pi_api_key_env(self):
b = _provider_config_bottle({
"template": "pi",
"settings": {
"provider": "openrouter",
"base_url": "https://openrouter.ai/api/v1",
"api": "openai-completions",
"api_key_env": "OPENROUTER_API_KEY",
"models": ["google/gemma-4-26b-a4b-it:free"],
},
})
self.assertEqual("OPENROUTER_API_KEY", b.agent_provider.settings["api_key_env"])
def test_settings_rejects_api_key_and_api_key_env_together(self):
with self.assertRaises(ManifestError):
_provider_config_bottle({
"template": "pi",
"settings": {
"api_key": "literal",
"api_key_env": "OPENROUTER_API_KEY",
},
})
def test_settings_rejected_for_claude(self):
with self.assertRaises(ManifestError):
_provider_config_bottle({