Skip to content

Core SDK Gap #2: Provider-specific dispatch scattered across core with feature-flag style branching #1305

@praisonai-triage-agent

Description

@praisonai-triage-agent

Gap #2: Provider-Specific Dispatch Scattered Across Core

Scope: src/praisonai-agents/praisonaiagents/llm/, agent/
Priority: High (depends on Gap #1)
Related: Part of issue #1302 architectural gaps

Problem Statement

The core contains inline if provider == X / elif provider == Y branching on model strings in dozens of hot-path locations instead of delegating to provider adapters. This creates feature-flag-style bloat that violates protocol-driven architecture and makes adding new providers require core edits.

Current Architecture Issues

Provider Branching Proliferation

In llm/llm.py alone:

  • 262 case-insensitive occurrences of gemini|claude|ollama|anthropic|openai|gpt
  • 40+ branching locations in chat loops

Key hotspots (line numbers from llm/llm.py):

  • 479 if self._is_ollama_provider():
  • 493 if self.model.startswith("ollama/"):
  • 1152 if is_ollama:
  • 1273 if not (self._is_ollama_provider() and iteration_count >= self.OLLAMA_SUMMARY_ITERATION_THRESHOLD):
  • 1326 if self.model.startswith("claude-"):
  • 1330 if any(self.model.startswith(prefix) for prefix in ["gemini-", "gemini/"]):
  • 1383 if self.prompt_caching and self._supports_prompt_caching() and self._is_anthropic_model():
  • 1551 if tool_name in gemini_internal_tools:
  • 2031 if use_streaming and formatted_tools and self._is_gemini_model():

Sync/Async Duplication Compounds the Problem

Provider logic appears in both execution paths:

  • Sync loop (2382, 2486, 2540, 2564, 2583, 2618, 2667, 2689)
  • Async loop (3235, 3690, 3712, 3725, 3746, 3761, 3782, 3904, 3924)

Beyond LLM Layer

Pattern repeats across core modules:

  • agent/deep_research_agent.py: if provider == "litellm" / elif provider == "gemini" at ~301-304, 1181-1190, 1249-1258, 1340, 1383, 1427
  • agent/chat_mixin.py: tool conversion checks with hasattr(tool, "to_openai_tool")

Architecture Violations

  • Protocol-driven core: Business logic contains provider-specific branches instead of delegating to adapters
  • Extensible: Adding new providers requires editing llm.py, deep_research_agent.py, chat_mixin.py
  • Simpler: Dual execution paths exist partly because provider quirks force different ordering

Proposed Solution

Introduce LLMProviderAdapter protocol with per-provider hooks:

class LLMProviderAdapter(Protocol):
    def supports_prompt_caching(self) -> bool: ...
    def should_summarize_tools(self, iter_count: int) -> bool: ...  # replaces OLLAMA_SUMMARY_ITERATION_THRESHOLD
    def format_tools(self, tools) -> list: ...                      # replaces Gemini internal tool branch
    def post_tool_iteration(self, state) -> None: ...               # replaces Ollama post-tool summary branch
    def supports_streaming_with_tools(self) -> bool: ...            # replaces Gemini streaming check
    def get_max_iteration_threshold(self) -> int: ...               # provider-specific limits

Architecture:

LLMProviderAdapter
  ├── OllamaAdapter     - handles OLLAMA_SUMMARY_ITERATION_THRESHOLD, post-tool summaries
  ├── AnthropicAdapter  - prompt caching, Claude-specific features  
  ├── GeminiAdapter     - internal tools, streaming limitations
  ├── OpenAIAdapter     - baseline behavior
  └── LiteLLMAdapter    - fallback for unknown providers

Implementation Strategy

  1. Register adapters once during LLM initialization based on model string
  2. Replace all provider branches with self._adapter.method() calls
  3. Zero if provider == logic in hot paths
  4. New providers ship as small adapter files; core untouched

Example Transformation

Before:

if self._is_ollama_provider() and iteration_count >= self.OLLAMA_SUMMARY_ITERATION_THRESHOLD:
    # Ollama-specific summarization logic
    pass

After:

if self._adapter.should_summarize_tools(iteration_count):
    # Generic summarization logic
    pass

Success Criteria

  • All 40+ provider branches in llm/llm.py replaced with adapter calls
  • Provider-specific constants moved to adapter implementations
  • New providers can be added without editing core files
  • Zero runtime provider string matching in hot paths
  • All existing functionality preserved

Implementation Files

Core Protocol:

  • llm/protocols.py - Add LLMProviderAdapter protocol
  • llm/adapters/__init__.py - Adapter registry and base classes

Provider Adapters:

  • llm/adapters/ollama.py - Ollama-specific behaviors
  • llm/adapters/anthropic.py - Claude/prompt caching features
  • llm/adapters/gemini.py - Internal tools, streaming quirks
  • llm/adapters/openai.py - Baseline adapter
  • llm/adapters/litellm.py - Fallback for unknown providers

Core Refactoring:

  • llm/llm.py - Remove all provider branches, use adapter delegation
  • agent/deep_research_agent.py - Remove provider conditionals
  • agent/chat_mixin.py - Clean tool conversion logic

Dependencies

  • Requires Gap Github actions fix #1 completion: Dual execution paths make this refactoring complex
  • Enables Gap Main #3: Clean provider abstraction helps with memory/knowledge adapter patterns

This issue is part of the larger architectural refactoring outlined in #1302. Provider adapter protocol eliminates feature-flag bloat and enables third-party provider extensions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions