feat: add first-class OpenAI-compatible custom endpoint path - #1198
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe change adds configurable OpenAI-compatible endpoint support through a factory and provider, including keyless authentication, capability defaults, integration coverage, unit tests, and quickstart documentation. ChangesOpenAI-compatible endpoint support
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests.
... and 35 files with indirect coverage changes 🚀 New features to boost your workflow:
|
bdb9c19 to
d985692
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/any_llm/any_llm.py`:
- Line 207: The public factory create_openai_compatible in
src/any_llm/any_llm.py:207 and the provider constructor in
src/any_llm/providers/openai/custom.py:39 both trigger ANN401 through open-ended
kwargs annotations. Update both sites to use the repository’s accepted typed
kwargs alias, or add narrowly scoped ANN401 suppressions with rationale that
these client options are intentionally open-ended.
In `@src/any_llm/providers/openai/custom.py`:
- Around line 20-23: Clarify the capability-error wording in
src/any_llm/providers/openai/custom.py lines 20-23: disabled capabilities are
rejected locally by AnyLLM, while enabled OpenAI-compatible defaults are passed
through to the endpoint, where endpoint errors may occur. Apply the same
distinction in docs/quickstart.md line 119; update both sites without changing
the capability flags or provider identity behavior.
In `@tests/unit/providers/test_openai_compatible_provider.py`:
- Around line 66-70: Update test_client_kwargs_are_forwarded to construct the
provider through AnyLLM.create_openai_compatible, passing timeout as a keyword
argument and retaining the timeout assertion. Ensure the test validates the
public factory’s kwargs-forwarding contract rather than directly instantiating
OpenAICompatibleProvider.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: deaa0461-20ab-4900-9e7e-11f1aa09cdc2
📒 Files selected for processing (4)
docs/quickstart.mdsrc/any_llm/any_llm.pysrc/any_llm/providers/openai/custom.pytests/unit/providers/test_openai_compatible_provider.py
Point any-llm at any OpenAI-compatible gateway that does not have a
dedicated provider, via AnyLLM.create_openai_compatible(name, api_base,
api_key). The returned provider reports the caller's chosen name as its
identity instead of masquerading as `openai`, and is usable exactly like
any other provider instance.
Previously the only way to reach an unlisted gateway was
AnyLLM.create("openai", api_base=...), which misreports the provider as
openai, and LLMProvider.from_string rejects any name not in the enum, so
users could not represent their gateway under its own name. This makes
"bring your own endpoint" a supported, documented feature so nobody is
blocked from using any-llm with their endpoint.
The endpoint may be keyless (local servers) or keyed (hosted gateways):
api_key falls back to the env var then to a placeholder, never raising
MissingApiKeyError. Capability flags follow the OpenAI-compatible
defaults from BaseOpenAIProvider.
Part of #1197.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d985692 to
7ec2136
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/any_llm/any_llm.py`:
- Line 207: Update create_openai_compatible to reject name values that are empty
or contain only whitespace before creating the dynamic subclass or assigning
PROVIDER_NAME, raising the established validation error type. Add a focused
error-case test covering blank and whitespace-only names.
In `@src/any_llm/providers/openai/custom.py`:
- Around line 33-42: Update OpenAICompatibleProvider.__init__ and
ENV_API_BASE_NAME consistently: either remove the unused advertised
environment-variable constant, or allow api_base to be optional so the base
resolver can honor OPENAI_COMPATIBLE_API_BASE before enforcing a
missing-endpoint error, and add coverage for the environment-based configuration
path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: c783d476-f5f8-4d84-879d-a64a366487c2
📒 Files selected for processing (4)
docs/quickstart.mdsrc/any_llm/any_llm.pysrc/any_llm/providers/openai/custom.pytests/unit/providers/test_openai_compatible_provider.py
| return cls._create_provider(provider, api_key=api_key, api_base=api_base, **kwargs) | ||
|
|
||
| @classmethod | ||
| def create_openai_compatible(cls, name: str, api_base: str, api_key: str | None = None, **kwargs: Any) -> AnyLLM: |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject empty custom provider names.
An empty or whitespace-only name is accepted and copied to PROVIDER_NAME, producing blank provider metadata and error messages. Validate the identifier before creating the dynamic subclass and add an error-case test.
Proposed validation
def create_openai_compatible(cls, name: str, api_base: str, api_key: str | None = None, **kwargs: Any) -> AnyLLM:
+ if not name.strip():
+ raise ValueError("name must not be empty")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def create_openai_compatible(cls, name: str, api_base: str, api_key: str | None = None, **kwargs: Any) -> AnyLLM: | |
| def create_openai_compatible(cls, name: str, api_base: str, api_key: str | None = None, **kwargs: Any) -> AnyLLM: | |
| if not name.strip(): | |
| raise ValueError("name must not be empty") |
🧰 Tools
🪛 Ruff (0.15.21)
[warning] 207-207: Dynamically typed expressions (typing.Any) are disallowed in **kwargs
(ANN401)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/any_llm/any_llm.py` at line 207, Update create_openai_compatible to
reject name values that are empty or contain only whitespace before creating the
dynamic subclass or assigning PROVIDER_NAME, raising the established validation
error type. Add a focused error-case test covering blank and whitespace-only
names.
Reject blank names in create_openai_compatible, drop the unused ENV_API_BASE_NAME constant so metadata no longer advertises an env var the constructor never consults, correct the capability-error wording (flag-gated capabilities such as batch raise NotImplementedError locally; forwarded calls surface the endpoint's own error), and route the kwargs-forwarding test through the public factory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/any_llm/providers/openai/custom.py (2)
37-39: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReject whitespace-only API bases.
if not api_baseaccepts values such as" ", deferring an invalid endpoint failure to client initialisation. Useif not api_base.strip()and extend the existing error-case test accordingly.Suggested validation
- if not api_base: + if not api_base.strip():🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/any_llm/providers/openai/custom.py` around lines 37 - 39, Update the api_base validation in OpenAICompatibleProvider to reject whitespace-only strings by checking the stripped value, while preserving the existing explicit-endpoint error. Extend the existing error-case test to cover a whitespace-only api_base.
36-36: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winAdd
@overrideto the constructor.
OpenAICompatibleProvider.__init__overridesAnyLLM.__init__, and overloaded methods in the provider Python code require@override. Add it immediately above the constructor definition.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/any_llm/providers/openai/custom.py` at line 36, Add the required `@override` decorator immediately above OpenAICompatibleProvider.__init__, preserving its existing signature and implementation.Source: Coding guidelines
docs/quickstart.md (1)
101-110: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winDocument the
OPENAI_COMPATIBLE_API_KEYfallback.When
api_keyis omitted, the provider readsOPENAI_COMPATIBLE_API_KEYbefore using the keyless placeholder. The section currently documents only an inline key or keyless mode, so hosted users may miss the supported environment configuration. As per the PR objectives, environment-based key selection is part of the supported contract.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/quickstart.md` around lines 101 - 110, Update the AnyLLM.create_openai_compatible documentation example or surrounding text to state that omitted api_key values use OPENAI_COMPATIBLE_API_KEY before falling back to the keyless placeholder. Preserve the existing inline-key and keyless usage guidance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@docs/quickstart.md`:
- Around line 101-110: Update the AnyLLM.create_openai_compatible documentation
example or surrounding text to state that omitted api_key values use
OPENAI_COMPATIBLE_API_KEY before falling back to the keyless placeholder.
Preserve the existing inline-key and keyless usage guidance.
In `@src/any_llm/providers/openai/custom.py`:
- Around line 37-39: Update the api_base validation in OpenAICompatibleProvider
to reject whitespace-only strings by checking the stripped value, while
preserving the existing explicit-endpoint error. Extend the existing error-case
test to cover a whitespace-only api_base.
- Line 36: Add the required `@override` decorator immediately above
OpenAICompatibleProvider.__init__, preserving its existing signature and
implementation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 57206973-f603-49b5-8d0e-6515feb0c93d
📒 Files selected for processing (4)
docs/quickstart.mdsrc/any_llm/any_llm.pysrc/any_llm/providers/openai/custom.pytests/unit/providers/test_openai_compatible_provider.py
… path Reaches OpenAI's real endpoint through create_openai_compatible using the key CI already holds, covering the community-provider verification bar from #1197: completion, streaming, and list_models. Skips when OPENAI_API_KEY is absent. Verified live locally: 3 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…karound
Point users at the custom path from the three places they look when a
gateway is not listed: the generated providers page preamble, the
README install section, and a rationale note in the quickstart section
itself. The note names the concrete problems with create("openai",
api_base=...): misreported identity, OPENAI_API_KEY silently sent to
the custom endpoint, and MissingApiKeyError on keyless local servers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep the quickstart section focused on how to use the custom path; the recommendation against the openai api_base workaround stays on the providers page and README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tbille this one is ready for you. Please review and merge whenever you're comfortable with it. Heads up that #1201 (the config registry) is stacked on this branch, so merging this one first keeps that diff clean. |
…ts (mozilla-ai#423) * fix(providers): honor the optional API key for keyless custom endpoints The dashboard's "Custom endpoint" form labels the API key optional, but any-llm rejects a keyless call to most providers (openai, anthropic, ...) with MissingApiKeyError when no key and no native env var are present. So a keyless local backend (vLLM, llama.cpp, Ollama) added through the form could neither be tested nor used, contradicting the "optional" label. Supply a harmless placeholder key when an instance points at a custom api_base and has no key anywhere (config and the provider's native env var both empty). The injection is purely additive: it only affects the case any-llm would already reject, and it never overrides a real key or the documented env-var fallback. A local server ignores the placeholder; a real one that needs a key rejects it, the same failure the operator would already get. Applied at both the dispatch path (get_provider_kwargs) and the pre-save "Test connection" path so they agree. Mirrors any-llm's own keyless tolerance (mozilla-ai/any-llm#1198). Fixes mozilla-ai#421 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(providers): detect compound env-key labels for keyless placeholder any-llm exposes ENV_API_KEY_NAME as a "/"-joined list of alternatives for some providers (gemini: "GEMINI_API_KEY/GOOGLE_API_KEY"), so os.getenv on the whole label always missed, letting the placeholder shadow a real env-var key. Split on "/" and treat each as a candidate. Also rename the redundant TestKeylessProviderConnectionTest class. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Description Writes down the provider policy from #1197 in CONTRIBUTING.md, so config-only gateway PRs have a documented path and the acceptance bar is consistent from one review to the next. Docs only, no code change. The `Adding a New Provider` section now leads with a decision step rather than assuming every provider needs a folder: - **No PR needed.** If the endpoint speaks the OpenAI API, `AnyLLM.create_openai_compatible(...)` (added in #1198) already covers it. That is the right answer for private gateways and self-hosted servers, and it means nobody is ever blocked. - **A registry row.** Config-only OpenAI-compatible gateways get one row in `src/any_llm/providers/registry.py` (added in #1201), with no folder, no `pyproject.toml` extra, and no `tests/conftest.py` model maps. - **A code folder.** Reserved for providers whose protocol requires behavior: custom auth, non-OpenAI request or response shapes, param translation, or model-list quirks. The rule is stated as the reviewer judging whether the protocol *requires* the code. Shipping an official SDK is not by itself a reason for a folder, and adding an override that is not needed does not turn a config-only gateway into a code provider. `Provider Tiers` documents verified vs community as a support promise rather than a statement about code shape, so a config-only provider we hold keys for stays verified. Community entries are verified live by the contributor at PR time and excluded from the integration matrix; CI cannot use repository secrets on fork PRs, so contribution-time verification is the only bar that is actually enforceable. The removal policy is written down too, on the grounds that cheap addition is only sustainable if removal is equally cheap. Section 2a gives the concrete row, flag discipline (do not set a flag you have not exercised against the live endpoint), and a copy-pasteable verification script covering completion, streaming, and `list_models`. Two details worth flagging for review: - **The enum caveat is real, and verified.** A registry-only row resolves through `AnyLLM.create("name")` and `get_provider_class("name")`, but the `"name:model"` string form raises `UnsupportedProviderError`, because `split_model_provider` returns `LLMProvider`. I confirmed this by injecting a row with no enum member. The checklist therefore tells contributors to add the `LLMProvider` entry as well. Widening that type is still a follow-up on #1197. - **Tier labeling in the docs is not claimed as done.** An earlier draft said community providers are labeled in the generated docs. They are not yet, so the text points at #1197 for the remaining rollout instead. Also repairs stale references in the existing checklist, since they sit in the section being rewritten: - `ProviderName` in `src/any_llm/provider.py` is now `LLMProvider` in `src/any_llm/constants.py` (that file does not exist). - Providers inherit `AnyLLM` from `any_llm.any_llm`, not a `Provider` class from `any_llm.provider`. - The `__init__.py` snippet used a non-existent import path; it is the provider package's own `__init__.py`. - Renaming the old `2. Implementation Checklist` heading to `2b` broke an in-page anchor further up the file, which is repointed. Happy to split the stale-reference cleanup into its own commit if you would rather review it separately. ## PR Type - 📚 Documentation ## Relevant issues Completes the `Update CONTRIBUTING.md with the acceptance rule` item of #1197. Does not close the issue; the two-tier docs listing, `provider:model` routing for registry-only names, and the open questions about registry knobs and shim lifetime remain. ## Checklist - [x] I understand the code I am submitting. - [ ] I have added unit tests that prove my fix/feature works - [x] I have run this code locally and verified it fixes the issue. - [x] New and existing tests pass locally - [x] Documentation was updated where necessary - [x] I have read and followed the [contribution guidelines](https://github.com/mozilla-ai/any-llm/blob/main/CONTRIBUTING.md) - [x] **AI Usage:** - [ ] No AI was used. - [x] AI was used for drafting/refactoring. - [ ] This is fully AI-generated. Notes on the checklist: no unit tests, since this is a documentation-only change. `tests/docs` only globs `docs/**/*.md`, so the Python snippets in CONTRIBUTING.md are not executed by the suite; I validated them by hand against the real API instead (`create_openai_compatible` signature, `AnyLLM.create` with an injected registry row, and `Model.id` for the `list_models` output). `pre-commit` and `pytest tests/docs` are clean. ## AI Usage Information - AI Model used: Claude Opus 5 - AI Developer Tool used: Claude Code - Any other info you'd like to share: Drafted by Claude through back and forth with @njbrake. The policy decisions and the reasoning are his; the prose is Claude's. The behavioral claims in the text were checked against the code rather than assumed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Documentation** - Updated provider contribution guidance with clearer support tiers, eligibility criteria, and decision rules. - Added instructions for configuration-only gateways, custom-code providers, and providers that do not require a pull request. - Clarified registry updates, capability flags, enum and package registration, implementation paths, and override requirements. - Expanded integration-testing and live-verification expectations for each support tier. - Added guidance on provider removal and refreshed project structure references. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Description
Adds a first-class, supported way to point any-llm at any OpenAI-compatible endpoint that does not have a dedicated provider, via a new factory:
Previously the only way to reach an unlisted gateway was
AnyLLM.create("openai", api_base=...), which misreports the provider identity asopenai, andLLMProvider.from_stringrejects any name not in the enum, so users could not represent their gateway under its own name. The returned provider reports the caller's chosennameand is usable exactly like any other provider instance.Design notes:
provider:modelrouting for community gateways is the follow-up registry work in the parent issue.api_keyfalls back toOPENAI_COMPATIBLE_API_KEY, then to a placeholder, so local servers are never blocked byMissingApiKeyError.BaseOpenAIProvider; calling a capability the endpoint does not implement surfaces the endpoint's own error. Explicit per-provider flags are the registry's job in a later PR.Implementation:
src/any_llm/providers/openai/custom.py:OpenAICompatibleProvider(BaseOpenAIProvider)src/any_llm/any_llm.py:AnyLLM.create_openai_compatible(name, api_base, api_key=None, **kwargs)tests/unit/providers/test_openai_compatible_provider.py: 11 tests (identity, base-URL binding, empty-api_base guard, keyless placeholder, explicit/env key precedence, capability defaults, kwargs forwarding, enum exclusion)docs/quickstart.md: "Custom OpenAI-compatible Endpoints" sectionTesting:
pytest tests/unit/providers/test_openai_compatible_provider.py: 11 passedpre-commit run --files <changed>: ruff, ruff-format, mypy strict, codespell all passPR Type
Relevant issues
Part of #1197 (first-class OpenAI-compatible custom path). Deliberately scoped to that one step; does not close the issue.
Checklist
AI Usage Information
AI Model used: Claude Opus 4.8
AI Developer Tool used: Claude Code
Any other info you'd like to share: Implemented by Claude via back-and-forth with @njbrake, who directed the scope (this is PR 1 of the Provider policy: two tiers, a config registry, and a first-class OpenAI-compatible path #1197 provider-policy rollout) and the design decisions (instance-based custom path, zero-knob registry to follow). Verified locally with unit tests and the pre-commit gate.
I am an AI Agent filling out this form (check box if true)
Summary by CodeRabbit
New Features
AnyLLM.create_openai_compatible.Documentation
Tests