Skip to content

fix(proxy): resolve router_settings.plugins dotted paths and load plugins from installed packages - #33644

Merged
krrish-berri-2 merged 1 commit into
litellm_internal_stagingfrom
litellm_resolve_routing_plugins
Jul 17, 2026
Merged

fix(proxy): resolve router_settings.plugins dotted paths and load plugins from installed packages#33644
krrish-berri-2 merged 1 commit into
litellm_internal_stagingfrom
litellm_resolve_routing_plugins

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

The reproduction config points router_settings.plugins at a dotted path whose module lives in an installed package (on PYTHONPATH, not next to the config), so it exercises both gaps at once

# /home/ubuntu/plugin_proof/cfg/config.yaml
model_list:
  - model_name: claude-smart
    litellm_params:
      model: anthropic/claude-sonnet-4-5-20250929
      api_key: os.environ/ANTHROPIC_API_KEY
router_settings:
  plugins:
    - lang_plugin.language_plugin_instance   # lang_plugin lives in an installed package on PYTHONPATH

Same curl for both runs, hitting the real Anthropic API

PYTHONPATH=/home/ubuntu/plugin_proof/installed_pkg \
  python litellm/proxy/proxy_cli.py --config .../config.yaml --detailed_debug --port <port>

curl -s http://localhost:<port>/v1/chat/completions \
  -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
  -d '{"model":"claude-smart","messages":[{"role":"user","content":"Hola, por favor responde en una frase: que es LiteLLM?"}]}'

Before the fix (base 4cfc987f56), the request fails because the dotted-path string is never resolved and the pipeline tries to await "lang_plugin.language_plugin_instance".run(context)

{"error":{"message":"Invalid request format: 'str' object has no attribute 'run'","type":"invalid_request_error","param":null,"code":"400"}}
AttributeError: 'str' object has no attribute 'run'

After the fix (commit c9aa000507), the plugin is resolved from the installed package at config load, runs in the routing pipeline, and the request returns a real Anthropic completion

[LanguageTagPlugin] ran; detected language=es; candidates=['anthropic/claude-sonnet-4-5-20250929']
'routing_plugin_signals': {'language-detector': {'language': 'es', 'source': 'installed-package'}}
CONTENT: LiteLLM es una biblioteca de Python que proporciona una interfaz unificada para llamar a diferentes APIs de modelos de lenguaje (como OpenAI, Anthropic, Cohere, etc.) usando el mismo formato de codigo.
MODEL: claude-smart

Type

🐛 Bug Fix

Changes

Two gaps surfaced while validating the routing-plugin pipeline against a live proxy

First, router_settings.plugins was passed straight through to Router(plugins=...) as raw config values, so dotted-path strings were never turned into RoutingPlugin instances. The Router then treated each string as a plugin and blew up at request time on await "some.path".run(context). The complexity-router path already resolved its plugins via resolve_complexity_router_plugins, so this factors that logic into a shared resolve_routing_plugins helper and calls it for router_settings.plugins too during config load, failing fast with a clear error if an entry does not implement the RoutingPlugin interface

Second, get_instance_fn (the dotted-path resolver behind both plugin paths and litellm_settings.callbacks) only ever looked for a local module file next to the config when config_file_path was set, and raised ImportError when that file was absent. That made it impossible to reference a plugin shipped as an installed Python package. It now prefers the local file when one exists and otherwise falls back to importlib.import_module, so module.instance resolves whether the module is a local file or an installed package. The existing remote-loading gate (s3://, gcs://) is untouched

Tests cover the installed-package fallback and local-file precedence in get_instance_fn, the new resolve_routing_plugins helper, and an end-to-end load_config regression asserting router_settings.plugins dotted paths land on the Router as live instances and that a bad entry raises at load time

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/605a563c77f94b5d94b792e89ce5ab7e
Requested by: @krrish-berri-2

…gins from installed packages

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@krrish-berri-2 krrish-berri-2 self-assigned this Jul 17, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two gaps in the routing-plugin pipeline: router_settings.plugins dotted-path strings are now resolved to live RoutingPlugin instances at config-load time (previously they were passed raw to the Router and blew up on the first request with 'str' object has no attribute 'run'), and get_instance_fn now falls back to importlib.import_module when no local module file exists next to the config file, enabling plugins shipped as installed packages to be referenced.

  • Refactors the existing resolve_complexity_router_plugins into a shared resolve_routing_plugins helper and calls it for both complexity_router_config.plugins and router_settings.plugins, failing fast with a clear error at startup rather than at request time.
  • Changes get_instance_fn so that when config_file_path is set but the computed local .py file is absent, it falls through to importlib.import_module instead of raising ImportError, with local file still taking precedence.
  • Adds focused unit and integration tests covering the new fallback path, local-wins-over-installed priority, and end-to-end load_config resolution.

Confidence Score: 4/5

The change is safe to merge; it resolves a real runtime crash and adds clear early-failure validation at config load time with no regressions to existing paths.

The refactoring is clean and the logic is correct throughout. The get_instance_fn fallback is intentional and properly scoped, the validation in resolve_routing_plugins correctly checks for async run beyond what the @runtime_checkable Protocol alone guarantees, and tests cover the new code paths without real network calls.

litellm/proxy/types_utils/utils.py — the fallback else branch now silently drops the local-file path from the error message; worth a second read to confirm the operator debugging experience is acceptable.

Important Files Changed

Filename Overview
litellm/proxy/types_utils/utils.py Removes the hard ImportError when a local file is absent next to the config and instead falls through to importlib.import_module; local-file precedence is preserved.
litellm/proxy/proxy_server.py Extracts resolve_routing_plugins from the former resolve_complexity_router_plugins and wires it into load_config for router_settings.plugins; validation and early-failure semantics are preserved correctly.
tests/test_litellm/proxy/proxy_server/test_proxy_config.py Adds four unit tests for resolve_routing_plugins and two async integration tests for load_config; all use tmp_path/monkeypatch with no real network calls.
tests/test_litellm/proxy/types_utils/test_get_instance_fn_runtime_gate.py Three new tests cover the installed-package fallback, local-file precedence, and the missing-everywhere ImportError; module names are unique enough to avoid sys.modules collisions in practice.

Reviews (1): Last reviewed commit: "fix(proxy): resolve router_settings.plug..." | Re-trigger Greptile

Comment on lines +78 to +95
" return context\n"
"\n"
"instance = _P()\n"
)
monkeypatch.syspath_prepend(str(pkg_dir))
config_dir = tmp_path / "cfg"
config_dir.mkdir()

result = get_instance_fn(
value="my_installed_plugin.instance",
config_file_path=str(config_dir / "config.yaml"),
)

assert type(result).__name__ == "_P"


def test_local_module_file_wins_over_installed_package(tmp_path, monkeypatch):
# A local module file next to the config must still take precedence over an

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 sys.modules pollution across tests

monkeypatch.syspath_prepend restores sys.path after the test but does not clear sys.modules. When get_instance_fn falls back to importlib.import_module("my_installed_plugin"), Python caches the module in sys.modules. If another test in the same process later calls importlib.import_module("my_installed_plugin") (e.g. after a different syspath_prepend), it will silently receive the stale cached instance from this test's tmp_path/site directory rather than a freshly imported one. The module names used here are unique enough to avoid real collisions, but a monkeypatch.delitem(sys.modules, "my_installed_plugin", raising=False) at the end of the test (or via a finally/fixture) would make the isolation explicit and protect against future name reuse.

Comment on lines 50 to 51
else:
# Dynamically import the module
module = importlib.import_module(module_name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Fallback import error loses local-path context

When both the local file check fails (file absent) and importlib.import_module also fails (package not installed), the ImportError handler on line 57-60 re-raises with "Could not import {instance_name} from {module_name}". Before this change, the error explicitly named the expected local file path ("Could not find module file /config/dir/module.py"), making it immediately obvious where the proxy looked. After the change, operators debugging a typo in a plugin path get no hint about the expected local-file location. Consider logging the attempted local path at debug level before falling through, or including it in the re-raised message.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_resolve_routing_plugins (c9aa000) with litellm_internal_staging (4cfc987)

Open in CodSpeed

@krrish-berri-2
krrish-berri-2 merged commit 8a4f380 into litellm_internal_staging Jul 17, 2026
127 of 128 checks passed
@krrish-berri-2
krrish-berri-2 deleted the litellm_resolve_routing_plugins branch July 17, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants