litellm-custom-provider-base-url-not-plumbed
I investigated the environment, cloned the project (/workspace/<project>), reproduced the bug, analyzed it, and verified the fix. The deliverable is written to /workspace/SOLUTION.md.
Key findings:
Root cause — In src/chimera/gateway.py, resolve_litellm_model() had branches only for LiteLLM-native providers (zai, openrouter, anthropic, google, deepseek, openai). A config-only provider like hermes fell through returning the raw catalog id with no api_base/custom_llm_provider, so LiteLLM raised BadRequestError: LLM Provider NOT provided. Grep confirmed providers.<name>.base_url was read only by config._is_local_base_url() (a keyless-loopback probe), and complete() passed neither the provider base_url nor its resolved api_key.
Fix (2 hunks, placed after every native branch / the litellm_model early return):
1. Add optional base_url to resolve_litellm_model, plus a generic branch returning ("openai/" + model_name.rsplit("/",1)[-1], {api_base: base_url, custom_llm_provider: "openai"}).
2. In LiteLLMGateway.complete(), look up self.config.providers[effective_provider], pass its base_url, and fall back to provider_cfg.api_key when api_keys has no entry (so api_key_env-resolved credentials authenticate).
Verification (actually run):
- RED against pre-fix gateway.py: 16 failed, 7 passed — matches the expected signature.
- GREEN against fixed code: 23 passed.
- The end-to-end test drives a real litellm.acompletion at a stdlib ThreadingHTTPServer stub on loopback and asserts the stub saw bare model glm-5.3-flash and Authorization: Bearer <key>.
- ruff check: clean.
Full write-up with the exact diff, reproduction script, and commands is in /workspace/SOLUTION.md.
# Evidence - Problem class: litellm-custom-provider-base-url-not-plumbed - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T16:33:31.282Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: adding an OpenAI-compatible provider to chimera.yaml purely by config (providers.<name>.base_url + api_key_env + a catalog model) fails at call time with litellm BadRequestError 'LLM Provider NOT provided. Pass in the LLM provider you are trying to call. You passed model=hermes/glm-5.3-flash'. Root cause: gateway.resolve_litellm_model handled only providers with a LiteLLM-native prefix (zai/deepseek hardcode api_base + custom_llm_provider; openrouter/anthropic/google/openai get a prefix) and fell through otherwise returning the catalog id with NO api_base, so config.providers[name].base_url never reached the LLM call - the config-only custom-endpoint path was dead despite the config schema advertising it. Diagnosis steps: (1) grep every read of providers[].base_url in the package - the only hit was _is_local_base_url, i.e. the base_url was used for a keyless-loopback check and nowhere else; (2) confirm the call site (LiteLLMGateway.complete) passes only api_key from config.api_keys, never the provider base_url; (3) reproduce with a real call, not a mock. Fix shape: add an optional base_url parameter threaded from complete() (which holds the config), and a generic branch mirroring the zai branch - return ('openai/' + bare model id after the last '/', {api_base: base_url, custom_llm_provider: 'openai'}), placed AFTER every native branch and after the explicit litellm_model early return so nothing native changes; also fall back to providers.<name>.api_key (what api_key_env resolves to at load time) when api_keys has no shortcut entry, since a generic provider authenticates only through that key. Prove it hermetically: a stdlib ThreadingHTTPServer stub on <ip-address> driven by a real litellm.acompletion asserting the stub saw the bare model id and the Bearer header, plus a RED run of the same tests against the pre-fix gateway.py (observed 16 failed / 7 passed). Lesson: a config schema that advertises a field is not evidence the field is read; grep the field's read sites before calling a feature config-only.", "environment": "<project> (Python 3.11, litellm gateway), tick #220, commit 8b5f687", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "litellm-custom-provider-base-url-not-plumbed", "provider": "openrouter", "solved_at": "2026-09-17T16:33:31.282Z", "version": ""}I investigated the environment, cloned the project (/workspace/<project>), reproduced the bug, analyzed it, and verified the fix. The deliverable is written to /workspace/SOLUTION.md.
Key findings:
Root cause — In src/chimera/gateway.py, resolve_litellm_model() had branches only for LiteLLM-native providers (zai, openrouter, anthropic, google, deepseek, openai). A config-only provider like hermes fell through returning the raw catalog id with no api_base/custom_llm_provider, so LiteLLM raised BadRequestError: LLM Provider NOT provided. Grep confirmed providers.<name>.base_url was read only by config._is_local_base_url() (a keyless-loopback probe), and complete() passed neither the provider base_url nor its resolved api_key.
Fix (2 hunks, placed after every native branch / the litellm_model early return):
1. Add optional base_url to resolve_litellm_model, plus a generic branch returning ("openai/" + model_name.rsplit("/",1)[-1], {api_base: base_url, custom_llm_provider: "openai"}).
2. In LiteLLMGateway.complete(), look up self.config.providers[effective_provider], pass its base_url, and fall back to provider_cfg.api_key when api_keys has no entry (so api_key_env-resolved credentials authenticate).
Verification (actually run):
- RED against pre-fix gateway.py: 16 failed, 7 passed — matches the expected signature.
- GREEN against fixed code: 23 passed.
- The end-to-end test drives a real litellm.acompletion at a stdlib ThreadingHTTPServer stub on loopback and asserts the stub saw bare model glm-5.3-flash and Authorization: Bearer <key>.
- ruff check: clean.
Full write-up with the exact diff, reproduction script, and commands is in /workspace/SOLUTION.md.
# Evidence - Problem class: litellm-custom-provider-base-url-not-plumbed - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T16:33:31.282Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: adding an OpenAI-compatible provider to chimera.yaml purely by config (providers.<name>.base_url + api_key_env + a catalog model) fails at call time with litellm BadRequestError 'LLM Provider NOT provided. Pass in the LLM provider you are trying to call. You passed model=hermes/glm-5.3-flash'. Root cause: gateway.resolve_litellm_model handled only providers with a LiteLLM-native prefix (zai/deepseek hardcode api_base + custom_llm_provider; openrouter/anthropic/google/openai get a prefix) and fell through otherwise returning the catalog id with NO api_base, so config.providers[name].base_url never reached the LLM call - the config-only custom-endpoint path was dead despite the config schema advertising it. Diagnosis steps: (1) grep every read of providers[].base_url in the package - the only hit was _is_local_base_url, i.e. the base_url was used for a keyless-loopback check and nowhere else; (2) confirm the call site (LiteLLMGateway.complete) passes only api_key from config.api_keys, never the provider base_url; (3) reproduce with a real call, not a mock. Fix shape: add an optional base_url parameter threaded from complete() (which holds the config), and a generic branch mirroring the zai branch - return ('openai/' + bare model id after the last '/', {api_base: base_url, custom_llm_provider: 'openai'}), placed AFTER every native branch and after the explicit litellm_model early return so nothing native changes; also fall back to providers.<name>.api_key (what api_key_env resolves to at load time) when api_keys has no shortcut entry, since a generic provider authenticates only through that key. Prove it hermetically: a stdlib ThreadingHTTPServer stub on <ip-address> driven by a real litellm.acompletion asserting the stub saw the bare model id and the Bearer header, plus a RED run of the same tests against the pre-fix gateway.py (observed 16 failed / 7 passed). Lesson: a config schema that advertises a field is not evidence the field is read; grep the field's read sites before calling a feature config-only.", "environment": "<project> (Python 3.11, litellm gateway), tick #220, commit 8b5f687", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "litellm-custom-provider-base-url-not-plumbed", "provider": "openrouter", "solved_at": "2026-09-17T16:33:31.282Z", "version": ""}