Skip to content

Conversation

@john-fante
Copy link

Pull Request: Multi-Provider Async Client for Hybrid Model Pipelines

💡 Description

This PR introduces a robust MultiProviderClient that enables seamless switching between different LLM providers within a single application flow. By parsing the provider-model_name prefix, the client automatically routes requests to the appropriate backend.

The primary motivation for this implementation is to support Hybrid Model Pipelines. In agentic workflows (specifically prompt optimization tasks like Agent Lightning's APO), it is often more efficient to use a high-reasoning model for the "Gradient" step while offloading the "Apply Edit" step to a smaller, faster, and more cost-effective model (e.g., Llama-3-8B via Groq).

✨ Technical Features

  • OpenAI SDK Parity: Implements a proxy structure (client.chat.completions.create) to ensure drop-in compatibility with existing OpenAI-based codebases.
  • Provider Routing: Intelligent parsing of model strings (e.g., google-gemini-2.0-flash) to select the correct client.
  • Lazy & Conditional Initialization: Only activates providers where environment variables (e.g., GROQ_API_KEY, GOOGLE_API_KEY) are present, preventing runtime errors.
  • Extensible: Supports runtime addition of custom_providers for proprietary or local (Ollama/vLLM) endpoints.

🛠 Supported Backends

Provider Environment Variable Example Model
Google GOOGLE_API_KEY google-gemini-2.0-flash
Groq GROQ_API_KEY meta-llama/llama-4-maverick-17b-128e-instruct
OpenAI OPENAI_API_KEY openai-gpt-4o
Azure AZURE_OPENAI_API_KEY azure-gpt-4
OpenRouter OPENROUTER_API_KEY openrouter-anthropic/claude-3

🚀 Usage Example (Optimizing with Smaller Models)

This setup allows using a powerful model for reasoning and a smaller/faster model for edits to save resources:

from multi_provider_client import MultiProviderClient
import agent_lightning as agl

client = MultiProviderClient()

# Hybrid approach: Large model for reasoning, Small model for editing
algo = agl.APO(
    client,
    gradient_model="google-gemini-2.0-flash",        # Strong reasoning
    apply_edit_model="groq-llama-3.1-8b-instant",   # Fast & Cheap for smaller edits
)

Copilot AI review requested due to automatic review settings February 1, 2026 18:27
@john-fante
Copy link
Author

@microsoft-github-policy-service agree

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an async MultiProviderClient that mimics the OpenAI Python SDK surface (client.chat.completions.create) while routing requests to different LLM backends based on a provider-<model> prefix.

Changes:

  • Introduces MultiProviderClient with lazy initialization of provider-specific AsyncOpenAI clients (Google, Groq, OpenAI, Azure, OpenRouter).
  • Implements model-string parsing (provider-...) to select the correct backend and strip the provider prefix from the model.
  • Adds an OpenAI-compatible proxy surface (chat.completions.create) for drop-in usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +102 to +110
if provider not in self.clients:
for name in self.clients:
if model.startswith(name + "-"):
provider = name
actual_model = model[len(name) + 1:]
break
else:
raise ValueError(f"Unknown provider: {provider}. Supported: {list(self.clients.keys())}")

Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

When a supported provider is not configured (e.g., model starts with google- but GOOGLE_API_KEY is not set), this raises Unknown provider, which is misleading. Consider distinguishing between "unsupported provider" and "provider supported but not configured" and include which env var(s) are required in the error message.

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +134
print("--- Multi Provider Client ---")
print(f"{provider.upper()}: {actual_model}")
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

print() calls in create() will spam stdout in library usage and can leak provider/model choices in production logs. Use the project logging facilities (or remove the output), ideally at a debug level so callers can control verbosity.

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +111
def _parse_model(self, model: str) -> tuple[str, str]:
"""Parse model name into provider and actual model name.

Args:
model: String in "provider-model_name" format

Returns:
(provider, actual_model_name) tuple
"""
if "-" not in model:
raise ValueError(f"Model format must be 'provider-model_name': {model}")

idx = model.find("-")
provider = model[:idx]
actual_model = model[idx + 1:]

if provider not in self.clients:
for name in self.clients:
if model.startswith(name + "-"):
provider = name
actual_model = model[len(name) + 1:]
break
else:
raise ValueError(f"Unknown provider: {provider}. Supported: {list(self.clients.keys())}")

return provider, actual_model
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

This introduces new routing/parsing behavior (_parse_model) and a new OpenAI-compatible proxy surface (chat.completions.create), but there are no tests covering provider selection, error cases (missing dash/unknown provider), or custom providers. Add unit tests (e.g., under tests/utils/) with a mocked AsyncOpenAI to verify routing and error messages without requiring real API keys.

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +83
if custom_providers:
for name, config in custom_providers.items():
api_key = config.get("api_key") or os.getenv(config.get("api_key_env", ""))
base_url = config.get("base_url")
self.clients[name] = AsyncOpenAI(api_key=api_key, base_url=base_url)
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

custom_providers entries are registered even when api_key/api_key_env resolves to an empty value and/or base_url is missing. This makes misconfiguration hard to diagnose (it will fail later during the first request). Validate required fields during __init__ and raise a clear ValueError, or explicitly document/support providers that do not require an API key.

Copilot uses AI. Check for mistakes.
john-fante and others added 2 commits February 1, 2026 21:44
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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.

1 participant