fix(anyllm): forward openai api_base/api_key to the any-llm backend (#942) (#954)

## Description

The any-llm backend ignored `--openai-api-url`, so requests against
custom OpenAI-compatible providers (vLLM, LiteLLM, xiaomimimo.com, etc.)
were sent to `api.openai.com` instead of the configured URL, returning
401s. This wires the configured URL all the way through to the any-llm
client.

Closes #942

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

There were two layers to the bug, both fixed here:

- The URL was never threaded to the backend. `create_proxy_backend()`
did not accept or forward the configured OpenAI URL, so `AnyLLMBackend`
was always constructed without an `api_base`. It now takes
`openai_api_url` and passes it through as `api_base`, wired from
`config.openai_api_url` in `server.py`.
- The backend never applied it. `AnyLLMBackend.__init__` stored
`self.api_base` and `self.api_key` but never used them;
`AnyLLM.create()` only received the provider. Both are now forwarded to
`AnyLLM.create()`, and only when set, so providers that rely on their
own env-var defaults (`OPENAI_API_KEY` / `OPENAI_BASE_URL`) are
unaffected.

Files touched:
- `headroom/providers/registry.py` — `create_proxy_backend()` gains an
`openai_api_url` parameter, passed to the any-llm backend as `api_base`.
- `headroom/proxy/server.py` — pass
`openai_api_url=config.openai_api_url` into `create_proxy_backend()`.
- `headroom/backends/anyllm.py` — forward `api_key`/`api_base` to
`AnyLLM.create()` when set.

Verified against `any-llm-sdk` 1.17.0, whose `AnyLLM.create(provider,
api_key=None, api_base=None, ...)` accepts both parameters.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
$ pytest tests/test_backend_anyllm.py tests/test_provider_registry_extended.py tests/test_provider_registry.py -q
tests/test_backend_anyllm.py ..............                              [ 43%]
tests/test_provider_registry_extended.py .......                         [ 65%]
tests/test_provider_registry.py ...........                              [100%]
32 passed

$ ruff check headroom/backends/anyllm.py headroom/providers/registry.py headroom/proxy/server.py tests/test_backend_anyllm.py tests/test_provider_registry_extended.py
All checks passed!
```

## Real Behavior Proof

- Environment: macOS (ARM64), Python 3.13, any-llm-sdk 1.17.0
- Exact command / steps: introspected `AnyLLM.create` signature from
any-llm-sdk 1.17.0 to confirm it accepts `api_base`, then ran the unit
suites above which assert the URL is threaded through
`create_proxy_backend` into `AnyLLM.create`.
- Observed result: with `openai_api_url` set, `AnyLLMBackend` is now
constructed with `api_base=<url>` and `AnyLLM.create()` receives it;
previously it received only the provider and the value was dropped.
- Not tested: live end-to-end request against a real custom
OpenAI-compatible endpoint (no credentials available in this
environment); mypy was not run locally.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

Documentation and CHANGELOG updates are N/A: this restores intended
behavior of an existing documented flag (`--openai-api-url`) rather than
adding new surface. `mypy` and live end-to-end testing were not run in
this environment.
This commit is contained in:
Aniruddh Jha 2026-06-15 18:07:43 +02:00 committed by GitHub
parent 90bdc676fa
commit a7ee8a60a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 122 additions and 10 deletions

View file

@ -41,13 +41,25 @@ class AnyLLMBackend(Backend):
)
self.provider = provider.lower()
self.api_key = api_key
self.api_base = api_base
# Normalize empty-string overrides (e.g. an env var set to "") to None
# so provider defaults stay active instead of forwarding a blank value.
self.api_key = api_key or None
self.api_base = api_base or None
# Create the AnyLLM instance once and reuse
self.llm = AnyLLM.create(self.provider)
# Create the AnyLLM instance once and reuse. api_key/api_base are only
# forwarded when set so providers keep their own env-var defaults
# (e.g. OPENAI_API_KEY / OPENAI_BASE_URL) otherwise.
create_kwargs: dict[str, Any] = {}
if self.api_key is not None:
create_kwargs["api_key"] = self.api_key
if self.api_base is not None:
create_kwargs["api_base"] = self.api_base
self.llm = AnyLLM.create(self.provider, **create_kwargs)
logger.info(f"any-llm backend initialized (provider={provider})")
logger.info(
f"any-llm backend initialized (provider={provider}, "
f"api_base={self.api_base or 'default'})"
)
@property
def name(self) -> str:

View file

@ -149,6 +149,7 @@ def create_proxy_backend(
anyllm_provider: str,
bedrock_region: str | None,
logger: logging.Logger,
openai_api_url: str | None = None,
anyllm_backend_cls: Any | None = None,
litellm_backend_cls: Any | None = None,
) -> Backend | None:
@ -160,7 +161,7 @@ def create_proxy_backend(
provider = anyllm_provider
try:
backend_cls = anyllm_backend_cls or _load_anyllm_backend()
instance = cast("Backend", backend_cls(provider=provider))
instance = cast("Backend", backend_cls(provider=provider, api_base=openai_api_url))
logger.info("any-llm backend enabled (provider=%s)", provider)
return instance
except ImportError as exc:

View file

@ -793,6 +793,7 @@ class HeadroomProxy(
anyllm_provider=config.anyllm_provider,
bedrock_region=config.bedrock_region,
logger=logger,
openai_api_url=config.openai_api_url,
anyllm_backend_cls=AnyLLMBackend,
litellm_backend_cls=LiteLLMBackend,
)

View file

@ -43,7 +43,7 @@ def make_backend(
class FakeAnyLLM:
@staticmethod
def create(requested_provider: str):
def create(requested_provider: str, **kwargs): # noqa: ANN003
assert requested_provider == provider
return fake_instance
@ -52,6 +52,76 @@ def make_backend(
return anyllm.AnyLLMBackend(provider=provider.upper()), fake_instance
def test_init_forwards_api_base_and_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
"""Regression for #942: custom api_base/api_key must reach AnyLLM.create."""
fake_instance = FakeAnyLLMInstance()
create_calls: list[dict[str, object]] = []
class FakeAnyLLM:
@staticmethod
def create(requested_provider: str, **kwargs): # noqa: ANN003
create_calls.append({"provider": requested_provider, **kwargs})
return fake_instance
monkeypatch.setattr(anyllm, "ANYLLM_AVAILABLE", True)
monkeypatch.setattr(anyllm, "AnyLLM", FakeAnyLLM)
backend = anyllm.AnyLLMBackend(
provider="openai",
api_key="sk-custom",
api_base="https://custom-provider.example/v1",
)
assert backend.api_base == "https://custom-provider.example/v1"
assert create_calls == [
{
"provider": "openai",
"api_key": "sk-custom",
"api_base": "https://custom-provider.example/v1",
}
]
def test_init_omits_unset_api_base_and_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
"""Unset overrides must not be forwarded, preserving provider env defaults."""
fake_instance = FakeAnyLLMInstance()
create_calls: list[dict[str, object]] = []
class FakeAnyLLM:
@staticmethod
def create(requested_provider: str, **kwargs): # noqa: ANN003
create_calls.append({"provider": requested_provider, **kwargs})
return fake_instance
monkeypatch.setattr(anyllm, "ANYLLM_AVAILABLE", True)
monkeypatch.setattr(anyllm, "AnyLLM", FakeAnyLLM)
anyllm.AnyLLMBackend(provider="openai")
assert create_calls == [{"provider": "openai"}]
def test_init_treats_empty_overrides_as_unset(monkeypatch: pytest.MonkeyPatch) -> None:
"""Empty-string api_base/api_key must not be forwarded (env var set to "")."""
fake_instance = FakeAnyLLMInstance()
create_calls: list[dict[str, object]] = []
class FakeAnyLLM:
@staticmethod
def create(requested_provider: str, **kwargs): # noqa: ANN003
create_calls.append({"provider": requested_provider, **kwargs})
return fake_instance
monkeypatch.setattr(anyllm, "ANYLLM_AVAILABLE", True)
monkeypatch.setattr(anyllm, "AnyLLM", FakeAnyLLM)
backend = anyllm.AnyLLMBackend(provider="openai", api_key="", api_base="")
assert backend.api_base is None
assert backend.api_key is None
assert create_calls == [{"provider": "openai"}]
def make_choice(
content: str = "hello", finish_reason: str = "stop", tool_calls=None, index: int = 0
):

View file

@ -102,7 +102,11 @@ def test_create_proxy_backend_uses_injected_backend_types() -> None:
anyllm_provider="groq",
bedrock_region=None,
logger=logger,
anyllm_backend_cls=lambda provider: {"kind": "anyllm", "provider": provider},
anyllm_backend_cls=lambda provider, api_base: {
"kind": "anyllm",
"provider": provider,
"api_base": api_base,
},
)
litellm = create_proxy_backend(
backend="bedrock",
@ -116,10 +120,32 @@ def test_create_proxy_backend_uses_injected_backend_types() -> None:
},
)
assert anyllm == {"kind": "anyllm", "provider": "groq"}
assert anyllm == {"kind": "anyllm", "provider": "groq", "api_base": None}
assert litellm == {"kind": "litellm", "provider": "bedrock", "region": "us-east-1"}
def test_create_proxy_backend_passes_openai_api_url_to_anyllm() -> None:
"""Regression for #942: --openai-api-url must reach the any-llm backend."""
logger = logging.getLogger("test")
anyllm = create_proxy_backend(
backend="anyllm",
anyllm_provider="openai",
bedrock_region=None,
logger=logger,
openai_api_url="https://custom-provider.example/v1",
anyllm_backend_cls=lambda provider, api_base: {
"provider": provider,
"api_base": api_base,
},
)
assert anyllm == {
"provider": "openai",
"api_base": "https://custom-provider.example/v1",
}
def test_create_proxy_backend_handles_missing_or_direct_backends(
caplog: pytest.LogCaptureFixture,
) -> None:
@ -138,7 +164,9 @@ def test_create_proxy_backend_handles_missing_or_direct_backends(
anyllm_provider="groq",
bedrock_region=None,
logger=logger,
anyllm_backend_cls=lambda provider: (_ for _ in ()).throw(ImportError("missing")),
anyllm_backend_cls=lambda provider, api_base: (_ for _ in ()).throw(
ImportError("missing")
),
)
assert direct is None