fix(anyllm): forward openai api_base/api_key to the any-llm backend (#942)#954
Merged
JerrettDavis merged 2 commits intoJun 15, 2026
Merged
Conversation
…eadroomlabs-ai#942) The any-llm backend ignored --openai-api-url, so requests against custom OpenAI-compatible providers were sent to api.openai.com instead. Two problems are fixed: - create_proxy_backend() never accepted or forwarded the configured OpenAI URL, so AnyLLMBackend was always built without an api_base. It now takes openai_api_url and passes it through, wired from the proxy config in server.py. - AnyLLMBackend 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 keep their own env-var defaults (OPENAI_API_KEY / OPENAI_BASE_URL). Adds regression tests covering both the threading through create_proxy_backend and the forwarding into AnyLLM.create.
Contributor
PR governanceThis PR follows the template and is marked ready for human review. |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes the anyllm backend ignoring --openai-api-url by threading the configured OpenAI-compatible base URL through create_proxy_backend() into AnyLLMBackend, and then into AnyLLM.create().
Changes:
- Add
openai_api_urlparameter tocreate_proxy_backend()and pass it asapi_basewhen constructing the any-llm backend. - Wire
config.openai_api_urlfrom the proxy server intocreate_proxy_backend(). - Forward
api_base/api_key(when provided) fromAnyLLMBackendintoAnyLLM.create(), with regression tests covering the threading behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
headroom/providers/registry.py |
Adds openai_api_url to create_proxy_backend() and forwards it to the any-llm backend as api_base. |
headroom/proxy/server.py |
Passes config.openai_api_url into create_proxy_backend(). |
headroom/backends/anyllm.py |
Forwards api_base/api_key into AnyLLM.create() when set. |
tests/test_provider_registry_extended.py |
Updates backend injection test helper signature and adds regression coverage for #942. |
tests/test_backend_anyllm.py |
Adds regression tests asserting api_base/api_key forwarding behavior into AnyLLM.create(). |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
An OPENAI_TARGET_API_URL env var set to "" propagates an empty string through ProxyConfig.openai_api_url. The previous `is not None` guard would forward that blank api_base to AnyLLM.create() and mask it in logs as "default", breaking requests against the real provider. Normalize empty overrides to None so provider env defaults stay active.
Merged
chopratejas
pushed a commit
that referenced
this pull request
Jun 16, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>0.26.0</summary> ## [0.26.0](v0.25.0...v0.26.0) (2026-06-16) ### Features * add Copilot BYOK provider wrapper utilities and CLI support ([#1041](#1041)) ([e67ee2a](e67ee2a)) * add dashboard agent usage stats ([#814](#814)) ([6d3f39f](6d3f39f)) * Add support for Mistral Vibe CLI ([#935](#935)) ([0932b8b](0932b8b)) * attribute reread waste to over-compression via marker check ([#901](#901)) ([f928576](f928576)) * **bedrock:** cross-region + Converse compression; bundle proxy binary in images ([#999](#999)) ([0dc2e1c](0dc2e1c)) * **dashboard:** surface compression-vs-cache net impact in Prefix Cache panel ([#913](#913)) ([2a4d300](2a4d300)) * **evals:** adversarial-input robustness grid for compressors ([#918](#918)) ([5939004](5939004)) * **parser:** detect re-issued identical tool calls as reread waste ([#909](#909)) ([7d4ae86](7d4ae86)) * **policy:** batch deep edits through one cache-bust ([#856](#856) P3a) ([#1015](#1015)) ([c2e52fe](c2e52fe)) * **policy:** consume net-cost mutation gate in ContentRouter ([#856](#856) P2) ([#905](#905)) ([553ade4](553ade4)) * **proxy:** compress AWS Bedrock InvokeModel requests via configurable upstream ([#720](#720)) ([7edb27a](7edb27a)) ### Bug Fixes * **anthropic:** strip styled Claude model ids ([#651](#651)) ([0c5c89d](0c5c89d)) * **anyllm:** forward openai api_base/api_key to the any-llm backend ([#942](#942)) ([#954](#954)) ([a7ee8a6](a7ee8a6)) * **cache:** guard None exemplar embeddings in dynamic detector ([#950](#950)) ([1ec9320](1ec9320)) * **cache:** name the missing piece in semantic detector guard ([#1018](#1018)) ([3b0bcee](3b0bcee)) * **ci:** check out repo in PR Governance label job ([#1021](#1021)) ([4558bc2](4558bc2)) * **ci:** make PR governance advisory ([#1047](#1047)) ([74dff94](74dff94)) * **codex:** compute waste signals on the OpenAI Responses path ([#898](#898)) ([b9e2761](b9e2761)) * **codex:** poll /wham/usage for subscription limits (handshake no longer sends x-codex-* headers) ([#924](#924)) ([8c00f71](8c00f71)) * **codex:** PR health label check state ([#986](#986)) ([99c874d](99c874d)) * **codex:** retag thread providers so history menu stays whole across the proxy boundary ([#1034](#1034)) ([74ae781](74ae781)) * **codex:** write canonical hooks feature flag and migrate deprecated codex_hooks ([#743](#743)) ([dff6a19](dff6a19)) * **compression:** convert tree-sitter byte offsets to char offsets ([#892](#892)) ([b1f700f](b1f700f)) * **compression:** correct JSON array item counting and entropy gate ([#887](#887)) ([d6f0f0f](d6f0f0f)) * **compression:** keep container bodies compressible in code handler ([#890](#890)) ([16ed73b](16ed73b)) * **compression:** measure short-value threshold on payload, not token ([#889](#889)) ([65b0e8c](65b0e8c)) * **compression:** use thread-local tree-sitter parsers in code handler ([#893](#893)) ([6cdb846](6cdb846)) * **gemini:** surface functionResponse payloads to waste-signal detection ([#897](#897)) ([9b0c840](9b0c840)) * **learn:** decode directory names with spaces in Windows project paths ([#997](#997)) ([#1027](#1027)) ([2d3701b](2d3701b)) * **learn:** scan subagent and workflow transcripts ([#1045](#1045)) ([0ddd4ed](0ddd4ed)) * **openclaw:** declare headroom_retrieve tool contract ([#947](#947)) ([7c8c909](7c8c909)) * **policy:** correct warm-cache penalty in net_mutation_gain to (S + dT) ([#903](#903)) ([0632eba](0632eba)) * **proxy:** add native Bedrock converse-stream route ([#917](#917)) ([b08ec15](b08ec15)) * **proxy:** keep codex image-generation WS turns alive through the relay ([#1000](#1000)) ([7dbbb40](7dbbb40)) * **proxy:** make budget enforcement actually work ([#885](#885)) ([a14ab45](a14ab45)) * **proxy:** read RTK gain stats globally by default ([#957](#957)) ([b70fccb](b70fccb)) * route v1internal code assist requests to cloudcode-pa.googleapis… ([#821](#821)) ([e20f16b](e20f16b)) * **serena:** stop the Serena dashboard popup and make --no-serena actually disable Serena ([#1003](#1003)) ([919379a](919379a)) * support Copilot Business subscription auth ([#641](#641)) ([0b4a4bd](0b4a4bd)) * wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy entrypoint ([#943](#943)) ([9b7b436](9b7b436)) * **wrap:** avoid duplicate top-level keys when injecting codex provider ([#884](#884)) ([dd22cfd](dd22cfd)) ### Code Refactoring * DRY cache logic, add thread safety, fix Bash exclusion ([#704](#704)) ([e36fccd](e36fccd)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Epicism
pushed a commit
to Epicism/headroom
that referenced
this pull request
Jun 17, 2026
…eadroomlabs-ai#942) (headroomlabs-ai#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 headroomlabs-ai#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.
studyzy
pushed a commit
to studyzy/headroom
that referenced
this pull request
Jun 24, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>0.26.0</summary> ## [0.26.0](headroomlabs-ai/headroom@v0.25.0...v0.26.0) (2026-06-16) ### Features * add Copilot BYOK provider wrapper utilities and CLI support ([headroomlabs-ai#1041](headroomlabs-ai#1041)) ([e67ee2a](headroomlabs-ai@e67ee2a)) * add dashboard agent usage stats ([headroomlabs-ai#814](headroomlabs-ai#814)) ([6d3f39f](headroomlabs-ai@6d3f39f)) * Add support for Mistral Vibe CLI ([headroomlabs-ai#935](headroomlabs-ai#935)) ([0932b8b](headroomlabs-ai@0932b8b)) * attribute reread waste to over-compression via marker check ([headroomlabs-ai#901](headroomlabs-ai#901)) ([f928576](headroomlabs-ai@f928576)) * **bedrock:** cross-region + Converse compression; bundle proxy binary in images ([headroomlabs-ai#999](headroomlabs-ai#999)) ([0dc2e1c](headroomlabs-ai@0dc2e1c)) * **dashboard:** surface compression-vs-cache net impact in Prefix Cache panel ([headroomlabs-ai#913](headroomlabs-ai#913)) ([2a4d300](headroomlabs-ai@2a4d300)) * **evals:** adversarial-input robustness grid for compressors ([headroomlabs-ai#918](headroomlabs-ai#918)) ([5939004](headroomlabs-ai@5939004)) * **parser:** detect re-issued identical tool calls as reread waste ([headroomlabs-ai#909](headroomlabs-ai#909)) ([7d4ae86](headroomlabs-ai@7d4ae86)) * **policy:** batch deep edits through one cache-bust ([headroomlabs-ai#856](headroomlabs-ai#856) P3a) ([headroomlabs-ai#1015](headroomlabs-ai#1015)) ([c2e52fe](headroomlabs-ai@c2e52fe)) * **policy:** consume net-cost mutation gate in ContentRouter ([headroomlabs-ai#856](headroomlabs-ai#856) P2) ([headroomlabs-ai#905](headroomlabs-ai#905)) ([553ade4](headroomlabs-ai@553ade4)) * **proxy:** compress AWS Bedrock InvokeModel requests via configurable upstream ([headroomlabs-ai#720](headroomlabs-ai#720)) ([7edb27a](headroomlabs-ai@7edb27a)) ### Bug Fixes * **anthropic:** strip styled Claude model ids ([headroomlabs-ai#651](headroomlabs-ai#651)) ([0c5c89d](headroomlabs-ai@0c5c89d)) * **anyllm:** forward openai api_base/api_key to the any-llm backend ([headroomlabs-ai#942](headroomlabs-ai#942)) ([headroomlabs-ai#954](headroomlabs-ai#954)) ([a7ee8a6](headroomlabs-ai@a7ee8a6)) * **cache:** guard None exemplar embeddings in dynamic detector ([headroomlabs-ai#950](headroomlabs-ai#950)) ([1ec9320](headroomlabs-ai@1ec9320)) * **cache:** name the missing piece in semantic detector guard ([headroomlabs-ai#1018](headroomlabs-ai#1018)) ([3b0bcee](headroomlabs-ai@3b0bcee)) * **ci:** check out repo in PR Governance label job ([headroomlabs-ai#1021](headroomlabs-ai#1021)) ([4558bc2](headroomlabs-ai@4558bc2)) * **ci:** make PR governance advisory ([headroomlabs-ai#1047](headroomlabs-ai#1047)) ([74dff94](headroomlabs-ai@74dff94)) * **codex:** compute waste signals on the OpenAI Responses path ([headroomlabs-ai#898](headroomlabs-ai#898)) ([b9e2761](headroomlabs-ai@b9e2761)) * **codex:** poll /wham/usage for subscription limits (handshake no longer sends x-codex-* headers) ([headroomlabs-ai#924](headroomlabs-ai#924)) ([8c00f71](headroomlabs-ai@8c00f71)) * **codex:** PR health label check state ([headroomlabs-ai#986](headroomlabs-ai#986)) ([99c874d](headroomlabs-ai@99c874d)) * **codex:** retag thread providers so history menu stays whole across the proxy boundary ([headroomlabs-ai#1034](headroomlabs-ai#1034)) ([74ae781](headroomlabs-ai@74ae781)) * **codex:** write canonical hooks feature flag and migrate deprecated codex_hooks ([headroomlabs-ai#743](headroomlabs-ai#743)) ([dff6a19](headroomlabs-ai@dff6a19)) * **compression:** convert tree-sitter byte offsets to char offsets ([headroomlabs-ai#892](headroomlabs-ai#892)) ([b1f700f](headroomlabs-ai@b1f700f)) * **compression:** correct JSON array item counting and entropy gate ([headroomlabs-ai#887](headroomlabs-ai#887)) ([d6f0f0f](headroomlabs-ai@d6f0f0f)) * **compression:** keep container bodies compressible in code handler ([headroomlabs-ai#890](headroomlabs-ai#890)) ([16ed73b](headroomlabs-ai@16ed73b)) * **compression:** measure short-value threshold on payload, not token ([headroomlabs-ai#889](headroomlabs-ai#889)) ([65b0e8c](headroomlabs-ai@65b0e8c)) * **compression:** use thread-local tree-sitter parsers in code handler ([headroomlabs-ai#893](headroomlabs-ai#893)) ([6cdb846](headroomlabs-ai@6cdb846)) * **gemini:** surface functionResponse payloads to waste-signal detection ([headroomlabs-ai#897](headroomlabs-ai#897)) ([9b0c840](headroomlabs-ai@9b0c840)) * **learn:** decode directory names with spaces in Windows project paths ([headroomlabs-ai#997](headroomlabs-ai#997)) ([headroomlabs-ai#1027](headroomlabs-ai#1027)) ([2d3701b](headroomlabs-ai@2d3701b)) * **learn:** scan subagent and workflow transcripts ([headroomlabs-ai#1045](headroomlabs-ai#1045)) ([0ddd4ed](headroomlabs-ai@0ddd4ed)) * **openclaw:** declare headroom_retrieve tool contract ([headroomlabs-ai#947](headroomlabs-ai#947)) ([7c8c909](headroomlabs-ai@7c8c909)) * **policy:** correct warm-cache penalty in net_mutation_gain to (S + dT) ([headroomlabs-ai#903](headroomlabs-ai#903)) ([0632eba](headroomlabs-ai@0632eba)) * **proxy:** add native Bedrock converse-stream route ([headroomlabs-ai#917](headroomlabs-ai#917)) ([b08ec15](headroomlabs-ai@b08ec15)) * **proxy:** keep codex image-generation WS turns alive through the relay ([headroomlabs-ai#1000](headroomlabs-ai#1000)) ([7dbbb40](headroomlabs-ai@7dbbb40)) * **proxy:** make budget enforcement actually work ([headroomlabs-ai#885](headroomlabs-ai#885)) ([a14ab45](headroomlabs-ai@a14ab45)) * **proxy:** read RTK gain stats globally by default ([headroomlabs-ai#957](headroomlabs-ai#957)) ([b70fccb](headroomlabs-ai@b70fccb)) * route v1internal code assist requests to cloudcode-pa.googleapis… ([headroomlabs-ai#821](headroomlabs-ai#821)) ([e20f16b](headroomlabs-ai@e20f16b)) * **serena:** stop the Serena dashboard popup and make --no-serena actually disable Serena ([headroomlabs-ai#1003](headroomlabs-ai#1003)) ([919379a](headroomlabs-ai@919379a)) * support Copilot Business subscription auth ([headroomlabs-ai#641](headroomlabs-ai#641)) ([0b4a4bd](headroomlabs-ai@0b4a4bd)) * wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy entrypoint ([headroomlabs-ai#943](headroomlabs-ai#943)) ([9b7b436](headroomlabs-ai@9b7b436)) * **wrap:** avoid duplicate top-level keys when injecting codex provider ([headroomlabs-ai#884](headroomlabs-ai#884)) ([dd22cfd](headroomlabs-ai@dd22cfd)) ### Code Refactoring * DRY cache logic, add thread safety, fix Bash exclusion ([headroomlabs-ai#704](headroomlabs-ai#704)) ([e36fccd](headroomlabs-ai@e36fccd)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The any-llm backend ignored
--openai-api-url, so requests against custom OpenAI-compatible providers (vLLM, LiteLLM, xiaomimimo.com, etc.) were sent toapi.openai.cominstead 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
Changes Made
There were two layers to the bug, both fixed here:
create_proxy_backend()did not accept or forward the configured OpenAI URL, soAnyLLMBackendwas always constructed without anapi_base. It now takesopenai_api_urland passes it through asapi_base, wired fromconfig.openai_api_urlinserver.py.AnyLLMBackend.__init__storedself.api_baseandself.api_keybut never used them;AnyLLM.create()only received the provider. Both are now forwarded toAnyLLM.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 anopenai_api_urlparameter, passed to the any-llm backend asapi_base.headroom/proxy/server.py— passopenai_api_url=config.openai_api_urlintocreate_proxy_backend().headroom/backends/anyllm.py— forwardapi_key/api_basetoAnyLLM.create()when set.Verified against
any-llm-sdk1.17.0, whoseAnyLLM.create(provider, api_key=None, api_base=None, ...)accepts both parameters.Testing
pytest)ruff check .)mypy headroom)Test Output
Real Behavior Proof
AnyLLM.createsignature from any-llm-sdk 1.17.0 to confirm it acceptsapi_base, then ran the unit suites above which assert the URL is threaded throughcreate_proxy_backendintoAnyLLM.create.openai_api_urlset,AnyLLMBackendis now constructed withapi_base=<url>andAnyLLM.create()receives it; previously it received only the provider and the value was dropped.Review Readiness
Checklist
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.mypyand live end-to-end testing were not run in this environment.