DSPEx Client Integration with ExLLM - Migration Strategy
Overview
This document outlines the comprehensive migration strategy for DSPEx’s client layer from the current custom implementation to ExLLM, a unified Elixir client library for Large Language Models. This migration will eliminate the instructor_lite
dependency and provide a more robust, standardized client foundation. Additionally, this integration includes Nx support for client analytics, response processing, and embedding operations.
Nx Integration for Client Analytics
Enhanced Client Performance with Nx
# lib/dspex/clients/nx_analytics.ex
defmodule DSPEx.Clients.NxAnalytics do
@moduledoc """
Nx-powered client analytics for performance monitoring and optimization.
Provides numerical analysis of client performance metrics, response quality,
and provider comparison using Nx tensor operations.
"""
import Nx.Defn
@doc """
Analyze client response times and performance patterns.
## Examples
iex> response_times = [120, 150, 89, 200, 95]
iex> NxAnalytics.analyze_performance(response_times)
%{mean_time: 130.8, p95: 200, stability_score: 0.78}
"""
def analyze_performance(response_times) when is_list(response_times) do
tensor = Nx.tensor(response_times)
analyze_performance_impl(tensor)
end
defn analyze_performance_impl(times) do
mean_time = Nx.mean(times)
std_time = Nx.standard_deviation(times)
min_time = Nx.reduce_min(times)
max_time = Nx.reduce_max(times)
# Calculate stability score (lower std = higher stability)
stability_score = 1.0 / (1.0 + std_time / mean_time)
%{
mean_time: mean_time,
std_time: std_time,
min_time: min_time,
max_time: max_time,
stability_score: stability_score
}
end
@doc """
Compare performance across different providers using statistical analysis.
"""
def compare_providers(provider_metrics) when is_map(provider_metrics) do
providers = Map.keys(provider_metrics)
comparisons =
for provider_a <- providers,
provider_b <- providers,
provider_a != provider_b do
metrics_a = Map.get(provider_metrics, provider_a, [])
metrics_b = Map.get(provider_metrics, provider_b, [])
comparison = compare_provider_pair(metrics_a, metrics_b)
{{provider_a, provider_b}, comparison}
end
|> Enum.into(%{})
# Overall ranking based on performance
rankings = calculate_provider_rankings(provider_metrics)
%{
pairwise_comparisons: comparisons,
rankings: rankings,
recommendation: recommend_best_provider(rankings)
}
end
# Additional implementation details...
# [Implementation continues as shown in previous examples]
end
Nx Configuration for Client Operations
# config/config.exs - Dependencies and Nx Configuration
config :dspex, :clients,
# Dependencies
client_library: :ex_llm,
# Nx backend configuration
nx_backend: {Nx.BinaryBackend, []},
# Performance monitoring settings
analytics: %{
enabled: true,
performance_tracking: true,
embedding_analysis: true,
health_monitoring: true,
window_size: 100,
anomaly_threshold: 0.4
},
# Provider comparison settings
provider_comparison: %{
enabled: true,
min_samples: 20,
confidence_threshold: 0.8,
auto_switch: false # Set to true for automatic provider switching
},
# Embedding processing
embeddings: %{
similarity_threshold: 0.7,
dimension_validation: true,
normalization: :l2
}
Current Client Architecture Analysis
✅ Current Implementation Strengths
1. DSPEx.Client (Core Module)
Location: lib/dspex/client.ex
(808 lines)
Features:
- SIMBA stability enhancements
- Response format normalization
- Telemetry integration with Foundation
- Test mode behavior management
- Circuit breaker patterns
- Error categorization
Key Capabilities:
@type response :: %{choices: [%{message: message()}]}
@type error_reason :: :timeout | :network_error | :invalid_response |
:api_error | :circuit_open | :rate_limited |
:invalid_messages | :provider_not_configured |
:unsupported_provider | :unexpected_result
2. DSPEx.ClientManager (GenServer)
Location: lib/dspex/client_manager.ex
(787 lines)
Features:
- Persistent stateful connections
- OTP supervision integration
- Client statistics tracking
- Connection pooling foundation
- Process-based isolation
State Management:
@type client_state :: :idle | :active | :throttled | :circuit_open
@type client_stats :: %{
requests_made: non_neg_integer(),
requests_successful: non_neg_integer(),
requests_failed: non_neg_integer(),
last_request_at: DateTime.t() | nil,
circuit_failures: non_neg_integer()
}
❌ Current Limitations & Problems
1. Provider Support Gaps
- Limited providers: Only OpenAI, Gemini, basic HTTP
- Inconsistent APIs: Different providers handled differently
- Configuration complexity: Custom config management for each provider
2. InstructorLite Dependency Issues
Files to Remove:
lib/dspex/adapters/instructor_lite_gemini.ex
(242 lines)lib/dspex/predict_structured.ex
(partial - structured output logic)- Mix dependency:
{:instructor_lite, "~> 0.3.0"}
Problems:
- Single provider limitation: Only works with Gemini
- Maintenance burden: Custom adapter implementation
- Limited schema support: Basic JSON schema generation
- Error handling complexity: Custom error translation
3. Testing Complexity
- Custom mock system:
DSPEx.MockClientManager
- Provider-specific mocking: Different mock strategies
- Scattered test utilities: Mock helpers spread across files
ExLLM Integration Benefits
🚀 ExLLM Advantages
Based on ExLLM documentation:
1. Unified Provider Support
Supported Providers (12+ providers):
:anthropic
- Claude models:openai
- GPT models:groq
- Fast inference:lmstudio
- Local models via LM Studio:mistral
- Mistral AI models:perplexity
- Search-augmented models:openrouter
- 300+ models from multiple providers:ollama
- Local models:bedrock
- AWS Bedrock (multiple providers):gemini
- Google Gemini models:xai
- X.AI Grok models:bumblebee
- Local models via Bumblebee:mock
- Built-in testing support
2. Advanced Features
- Streaming Support: Built-in streaming with error recovery
- Function Calling: Unified interface for tool use
- Model Discovery: Query and compare model capabilities
- Automatic Retries: Exponential backoff with provider-specific policies
- Cost Tracking: Automatic usage and cost calculation
- Context Management: Automatic message truncation
- Session Management: Conversation state tracking
- Structured Outputs: Schema validation via instructor integration
- No Process Dependencies: Pure functional core
3. Configuration Flexibility
# Environment variables (automatic)
export ANTHROPIC_API_KEY="api-..."
export OPENAI_API_KEY="sk-..."
# Static configuration
config = %{
anthropic: %{api_key: "api-...", model: "claude-3-5-sonnet-20241022"},
openai: %{api_key: "sk-...", model: "gpt-4"},
openrouter: %{api_key: "sk-or-...", model: "openai/gpt-4o"}
}
# Custom configuration provider
defmodule MyConfigProvider do
@behaviour ExLLM.Infrastructure.ConfigProvider
def get([:anthropic, :api_key]), do: MyApp.get_secret("anthropic_key")
def get(_), do: nil
end
4. Built-in Testing
# Mock provider included
{:ok, response} = ExLLM.chat(:mock, messages)
# Check if configured
if ExLLM.configured?(:anthropic) do
{:ok, response} = ExLLM.chat(:anthropic, messages)
end
Migration Strategy
Phase 1: Dependency Management (Week 1)
1.1 Update mix.exs Dependencies
# REMOVE
{:instructor_lite, "~> 0.3.0"},
# ADD
{:ex_llm, "~> 0.8.1"}, # Unified LLM client library
{:nx, "~> 0.6"}, # Numerical computing for analytics and embeddings
1.2 Remove InstructorLite Files
Files to Delete:
lib/dspex/adapters/instructor_lite_gemini.ex
- Update
lib/dspex/predict_structured.ex
to remove InstructorLite logic
1.3 Update Application Configuration
# config/config.exs
config :dspex,
# Remove old client configuration
client_provider: ExLLM, # New provider
default_model_config: %{
anthropic: %{model: "claude-3-5-sonnet-20241022"},
openai: %{model: "gpt-4"},
gemini: %{model: "gemini-pro"}
}
Phase 2: Client Behaviour Implementation (Week 1)
2.1 Define DSPEx.Client Behaviour
# lib/dspex/client.ex - Add behaviour definition
defmodule DSPEx.Client do
@type message :: %{role: String.t(), content: String.t()}
@type request_options :: %{
optional(:provider) => atom(),
optional(:model) => String.t(),
optional(:temperature) => float(),
optional(:max_tokens) => pos_integer(),
optional(:correlation_id) => String.t(),
optional(:timeout) => pos_integer()
}
@type response :: %{choices: [%{message: message()}]}
@type error_reason :: :timeout | :network_error | :invalid_response |
:api_error | :circuit_open | :rate_limited |
:invalid_messages | :provider_not_configured |
:unsupported_provider | :unexpected_result
@callback request(messages :: [message()]) ::
{:ok, response()} | {:error, error_reason()}
@callback request(messages :: [message()], opts :: request_options()) ::
{:ok, response()} | {:error, error_reason()}
@callback request(client_id :: atom(), messages :: [message()], opts :: request_options()) ::
{:ok, response()} | {:error, error_reason()}
# Keep existing functions as default implementation
end
2.2 Create ExLLM Adapter
# lib/dspex/clients/ex_llm_client.ex
defmodule DSPEx.Clients.ExLLMClient do
@moduledoc """
ExLLM-based client implementation with DSPEx compatibility.
Provides a DSPEx.Client behaviour implementation using ExLLM as the
underlying provider, with response format normalization and error mapping.
"""
@behaviour DSPEx.Client
require Logger
alias DSPEx.Services.ConfigManager
@impl DSPEx.Client
def request(messages) do
request(messages, %{})
end
@impl DSPEx.Client
def request(messages, options) do
request(:default, messages, options)
end
@impl DSPEx.Client
def request(client_id, messages, options) do
# Extract provider and configuration
provider = resolve_provider(client_id, options)
# Convert DSPEx options to ExLLM options
ex_llm_options = convert_options(options)
# Add correlation tracking
correlation_id = Map.get(options, :correlation_id) || generate_correlation_id()
# Emit telemetry start
start_time = System.monotonic_time()
emit_telemetry_start(provider, correlation_id, messages, options)
# Execute ExLLM request with error mapping
result = case ExLLM.configured?(provider) do
true ->
execute_ex_llm_request(provider, messages, ex_llm_options)
false ->
{:error, :provider_not_configured}
end
# Emit telemetry stop
duration = System.monotonic_time() - start_time
emit_telemetry_stop(provider, correlation_id, result, duration)
result
end
# Private functions
defp execute_ex_llm_request(provider, messages, options) do
case ExLLM.chat(provider, messages, options) do
{:ok, %ExLLM.Types.ChatResponse{} = response} ->
# Normalize ExLLM response to DSPEx format
{:ok, normalize_ex_llm_response(response)}
{:error, reason} ->
# Map ExLLM errors to DSPEx error reasons
{:error, map_ex_llm_error(reason)}
end
end
defp normalize_ex_llm_response(%ExLLM.Types.ChatResponse{content: content, role: role}) do
%{
choices: [
%{
message: %{
role: role || "assistant",
content: content
}
}
]
}
end
defp convert_options(dspex_options) do
# Convert DSPEx options to ExLLM format
dspex_options
|> Map.take([:model, :temperature, :max_tokens, :timeout])
|> Enum.into([])
end
defp map_ex_llm_error(:timeout), do: :timeout
defp map_ex_llm_error(:network_error), do: :network_error
defp map_ex_llm_error(:invalid_response), do: :invalid_response
defp map_ex_llm_error(:api_error), do: :api_error
defp map_ex_llm_error(_), do: :unexpected_result
defp resolve_provider(client_id, options) do
cond do
provider = Map.get(options, :provider) -> provider
client_id != :default -> client_id
true -> Application.get_env(:dspex, :default_provider, :openai)
end
end
defp generate_correlation_id do
"exllm-#{System.unique_integer([:positive])}"
end
defp emit_telemetry_start(provider, correlation_id, messages, options) do
:telemetry.execute(
[:dspex, :client, :request, :start],
%{system_time: System.system_time()},
%{
provider: provider,
correlation_id: correlation_id,
message_count: length(messages),
temperature: Map.get(options, :temperature),
client_type: :ex_llm
}
)
end
defp emit_telemetry_stop(provider, correlation_id, result, duration) do
success = match?({:ok, _}, result)
:telemetry.execute(
[:dspex, :client, :request, :stop],
%{duration: duration, success: success},
%{
provider: provider,
correlation_id: correlation_id,
client_type: :ex_llm
}
)
end
end
Phase 3: Enhanced Client Manager (Week 2)
3.1 Update ClientManager for ExLLM
# lib/dspex/client_manager.ex - Enhanced with ExLLM
defmodule DSPEx.ClientManager do
use GenServer
@behaviour DSPEx.Client
# Add ExLLM session management
defstruct [
:provider,
:config,
:state,
:stats,
:ex_llm_session # New: ExLLM session for conversation tracking
]
@impl DSPEx.Client
def request(client_pid, messages, options) when is_pid(client_pid) do
GenServer.call(client_pid, {:request, messages, options}, get_timeout(options))
end
def request(messages, options) do
# Use ExLLM directly for stateless requests
DSPEx.Clients.ExLLMClient.request(messages, options)
end
def request(client_id, messages, options) do
DSPEx.Clients.ExLLMClient.request(client_id, messages, options)
end
# Enhanced session management with ExLLM
def start_session(provider, opts \\ []) do
GenServer.start_link(__MODULE__, {provider, opts})
end
def chat_with_session(client_pid, content, options \\ []) do
GenServer.call(client_pid, {:session_chat, content, options})
end
def get_session_history(client_pid, limit \\ nil) do
GenServer.call(client_pid, {:get_session_history, limit})
end
# GenServer callbacks with ExLLM integration
@impl GenServer
def init({provider, config}) do
# Create ExLLM session
session_opts = [
provider: provider,
model: get_model_config(provider, config)
]
case ExLLM.new_session(provider, session_opts) do
{:ok, ex_llm_session} ->
state = %__MODULE__{
provider: provider,
config: config,
state: :idle,
stats: initial_stats(),
ex_llm_session: ex_llm_session
}
{:ok, state}
{:error, reason} ->
{:stop, {:failed_to_create_session, reason}}
end
end
@impl GenServer
def handle_call({:session_chat, content, options}, _from, state) do
# Use ExLLM session for conversation tracking
case ExLLM.chat_with_session(state.ex_llm_session, content, options) do
{:ok, response} ->
# Update stats and normalize response
updated_state = update_stats(state, :success)
normalized_response = normalize_session_response(response)
{:reply, {:ok, normalized_response}, updated_state}
{:error, reason} ->
updated_state = update_stats(state, :failure)
{:reply, {:error, map_ex_llm_error(reason)}, updated_state}
end
end
@impl GenServer
def handle_call({:get_session_history, limit}, _from, state) do
messages = ExLLM.get_session_messages(state.ex_llm_session, limit)
{:reply, {:ok, messages}, state}
end
# Helper functions
defp normalize_session_response(response) do
# Convert ExLLM session response to DSPEx format
%{
choices: [
%{
message: %{
role: "assistant",
content: response.content
}
}
],
session_stats: %{
total_tokens: response.usage.total_tokens || 0,
prompt_tokens: response.usage.prompt_tokens || 0,
completion_tokens: response.usage.completion_tokens || 0
}
}
end
end
Phase 4: Structured Output Migration (Week 2-3)
4.1 Replace InstructorLite with ExLLM Structured Outputs
# lib/dspex/adapters/ex_llm_structured.ex
defmodule DSPEx.Adapters.ExLLMStructured do
@moduledoc """
DSPEx adapter using ExLLM's built-in structured output capabilities.
Replaces the InstructorLite integration with ExLLM's native structured
output support, providing better performance and broader provider support.
"""
@behaviour DSPEx.Adapter
@impl DSPEx.Adapter
def format_messages(signature, inputs) do
# Build prompt with schema instructions
with {:ok, prompt} <- build_structured_prompt(signature, inputs),
{:ok, schema} <- generate_json_schema(signature) do
messages = [%{role: "user", content: prompt}]
# Add structured output metadata
metadata = %{
json_schema: schema,
output_fields: signature.output_fields(),
signature_type: :structured
}
{:ok, {messages, metadata}}
end
end
@impl DSPEx.Adapter
def parse_response(signature, {response, metadata}) do
case Map.get(metadata, :signature_type) do
:structured ->
parse_structured_response(signature, response, metadata)
_ ->
parse_standard_response(signature, response)
end
end
# Make structured request using ExLLM
def request_structured(provider, signature, inputs, options \\ []) do
with {:ok, {messages, metadata}} <- format_messages(signature, inputs),
{:ok, ex_llm_options} <- prepare_structured_options(metadata, options),
{:ok, response} <- ExLLM.chat(provider, messages, ex_llm_options) do
parse_response(signature, {response, metadata})
end
end
# Private functions
defp build_structured_prompt(signature, inputs) do
instructions = signature.instructions()
input_text = format_inputs(signature, inputs)
schema_text = format_schema_instructions(signature)
prompt = """
#{instructions}
Input:
#{input_text}
Please respond with a valid JSON object matching this schema:
#{schema_text}
Ensure your response is valid JSON and includes all required fields.
"""
{:ok, prompt}
end
defp generate_json_schema(signature) do
output_fields = signature.output_fields()
properties =
output_fields
|> Enum.map(fn field -> {to_string(field), infer_field_schema(field)} end)
|> Enum.into(%{})
schema = %{
type: "object",
required: Enum.map(output_fields, &to_string/1),
properties: properties,
additionalProperties: false
}
{:ok, schema}
end
defp infer_field_schema(:confidence), do: %{type: "string", enum: ["low", "medium", "high"]}
defp infer_field_schema(:reasoning), do: %{type: "string", description: "Step-by-step reasoning"}
defp infer_field_schema(:score), do: %{type: "number", minimum: 0, maximum: 1}
defp infer_field_schema(:answer), do: %{type: "string", description: "The main answer"}
defp infer_field_schema(_), do: %{type: "string"}
defp prepare_structured_options(metadata, options) do
# ExLLM structured output options
ex_llm_options = options
|> Keyword.put(:response_format, %{type: "json_object"})
|> Keyword.put(:json_schema, metadata.json_schema)
{:ok, ex_llm_options}
end
defp parse_structured_response(signature, response, _metadata) do
case response.content do
content when is_binary(content) ->
case Jason.decode(content) do
{:ok, json_data} when is_map(json_data) ->
# Convert string keys to atom keys for consistency
atom_data = for {k, v} <- json_data, into: %{}, do: {String.to_existing_atom(k), v}
validate_structured_output(signature, atom_data)
{:error, _} ->
{:error, :invalid_json_response}
end
_ ->
{:error, :missing_content}
end
end
defp validate_structured_output(signature, data) do
output_fields = signature.output_fields()
missing_fields = output_fields -- Map.keys(data)
case missing_fields do
[] -> {:ok, data}
_ -> {:error, {:missing_fields, missing_fields}}
end
end
end
4.2 Update Predict Structured
# lib/dspex/predict_structured.ex - Remove InstructorLite, use ExLLM
defmodule DSPEx.PredictStructured do
@moduledoc """
Structured prediction using ExLLM's native structured output capabilities.
Provides guaranteed JSON schema compliance using ExLLM's built-in
structured output support across multiple providers.
"""
@behaviour DSPEx.Program
defstruct [
:signature,
:provider,
:adapter,
:model,
:temperature,
:max_tokens,
:demonstrations
]
def new(signature, provider, opts \\ []) do
%__MODULE__{
signature: signature,
provider: provider,
adapter: DSPEx.Adapters.ExLLMStructured,
model: Keyword.get(opts, :model),
temperature: Keyword.get(opts, :temperature, 0.7),
max_tokens: Keyword.get(opts, :max_tokens, 1000),
demonstrations: Keyword.get(opts, :demonstrations, [])
}
end
@impl DSPEx.Program
def forward(program, inputs, opts \\ []) do
# Use ExLLM structured adapter
request_options = [
model: program.model,
temperature: program.temperature,
max_tokens: program.max_tokens
] |> Keyword.merge(opts)
DSPEx.Adapters.ExLLMStructured.request_structured(
program.provider,
program.signature,
inputs,
request_options
)
end
end
Phase 5: Testing Infrastructure (Week 3)
5.1 Update Mock Infrastructure
# test/support/ex_llm_mocks.ex
defmodule DSPEx.ExLLMMocks do
@moduledoc """
Mock support for ExLLM integration testing.
"""
import Mox
def setup_ex_llm_mock(provider \\ :openai, responses \\ []) do
# Mock ExLLM.chat/3
expect(ExLLM.Mock, :chat, fn ^provider, _messages, _opts ->
response = get_mock_response(responses)
{:ok, %ExLLM.Types.ChatResponse{
content: response,
role: "assistant",
usage: %{total_tokens: 100, prompt_tokens: 50, completion_tokens: 50}
}}
end)
# Mock ExLLM.configured?/1
expect(ExLLM.Mock, :configured?, fn ^provider -> true end)
end
def setup_ex_llm_session_mock(provider \\ :openai) do
session = %ExLLM.Types.Session{
provider: provider,
messages: [],
metadata: %{}
}
expect(ExLLM.Mock, :new_session, fn ^provider, _opts ->
{:ok, session}
end)
expect(ExLLM.Mock, :chat_with_session, fn _session, content, _opts ->
{:ok, %ExLLM.Types.ChatResponse{
content: "Mock response to: #{content}",
role: "assistant",
usage: %{total_tokens: 75}
}}
end)
end
def setup_structured_output_mock(json_response) do
expect(ExLLM.Mock, :chat, fn _provider, _messages, opts ->
# Check if structured output is requested
case Keyword.get(opts, :response_format) do
%{type: "json_object"} ->
{:ok, %ExLLM.Types.ChatResponse{
content: Jason.encode!(json_response),
role: "assistant"
}}
_ ->
{:ok, %ExLLM.Types.ChatResponse{
content: "Standard response",
role: "assistant"
}}
end
end)
end
defp get_mock_response([]), do: "Default mock response"
defp get_mock_response([response | _]), do: response
end
5.2 Update Behaviour Mocks
# test/support/behaviour_mocks.exs - Update for ExLLM
defmodule DSPEx.BehaviourMocks do
import Mox
def setup_ex_llm_client_mock(provider \\ :openai) do
expect(DSPEx.MockClient, :request, fn messages, opts ->
# Simulate ExLLM client behavior
content = generate_contextual_response(messages)
{:ok, %{
choices: [
%{
message: %{
role: "assistant",
content: content
}
}
]
}}
end)
end
def setup_structured_client_mock(json_output) do
expect(DSPEx.MockClient, :request, fn _messages, opts ->
# Check if structured output is expected
case Map.get(opts, :response_format) do
%{type: "json_object"} ->
{:ok, %{
choices: [
%{
message: %{
role: "assistant",
content: Jason.encode!(json_output)
}
}
]
}}
_ ->
setup_ex_llm_client_mock()
end
end)
end
defp generate_contextual_response(messages) do
last_message = List.last(messages)
content = Map.get(last_message, :content, "")
cond do
String.contains?(content, "JSON") -> ~s({"answer": "mock", "confidence": "high"})
String.contains?(content, "structured") -> ~s({"result": "structured response"})
String.contains?(content, "2+2") -> "4"
true -> "Mock response for: #{String.slice(content, 0, 30)}"
end
end
end
Phase 6: Configuration & Environment (Week 3-4)
6.1 Update Application Configuration
# config/config.exs
config :dspex,
# Client configuration
client_provider: DSPEx.Clients.ExLLMClient,
default_provider: :openai,
# ExLLM default configuration
ex_llm_config: %{
anthropic: %{model: "claude-3-5-sonnet-20241022"},
openai: %{model: "gpt-4"},
gemini: %{model: "gemini-pro"},
groq: %{model: "llama2-70b-4096"},
ollama: %{model: "llama2", base_url: "http://localhost:11434"}
}
# config/test.exs
config :dspex,
client_provider: DSPEx.MockClient,
test_mode: :mock
# Use ExLLM mock provider
config :ex_llm,
default_provider: :mock
6.2 Environment Setup Guide
# .env.example
# OpenAI
export OPENAI_API_KEY="sk-..."
# Anthropic Claude
export ANTHROPIC_API_KEY="api-..."
# Google Gemini
export GOOGLE_API_KEY="your-key"
# Groq
export GROQ_API_KEY="gsk-..."
# OpenRouter (300+ models)
export OPENROUTER_API_KEY="sk-or-..."
# Local Ollama
export OLLAMA_API_BASE="http://localhost:11434"
# AWS Bedrock
export AWS_ACCESS_KEY_ID="your-key"
export AWS_SECRET_ACCESS_KEY="your-secret"
# X.AI Grok
export XAI_API_KEY="xai-..."
Phase 7: Documentation & Migration (Week 4)
7.1 Migration Documentation
# guides/migrating_from_instructor_lite.md
defmodule MigrationGuide do
@moduledoc """
# Migrating from InstructorLite to ExLLM
## Before (InstructorLite)
```elixir
# Limited to Gemini only
program = DSPEx.PredictStructured.new(
QASignature,
:gemini,
adapter: DSPEx.Adapters.InstructorLiteGemini
)
After (ExLLM)
# Works with all ExLLM providers
program = DSPEx.PredictStructured.new(QASignature, :anthropic)
program = DSPEx.PredictStructured.new(QASignature, :openai)
program = DSPEx.PredictStructured.new(QASignature, :gemini)
program = DSPEx.PredictStructured.new(QASignature, :groq)
Benefits
- 12+ provider support vs 1
- Built-in streaming and function calling
- Better error handling and retries
- Native cost tracking
- Session management
- No custom adapter maintenance """ end
#### 7.2 Usage Examples
```elixir
# examples/ex_llm_client_examples.ex
defmodule DSPEx.Examples.ExLLMClient do
@moduledoc """
Examples of using DSPEx with ExLLM integration.
"""
# Basic chat across providers
def multi_provider_example do
messages = [%{role: "user", content: "What is Elixir?"}]
# OpenAI
{:ok, response1} = DSPEx.Client.request(messages, %{provider: :openai})
# Anthropic Claude
{:ok, response2} = DSPEx.Client.request(messages, %{provider: :anthropic})
# Google Gemini
{:ok, response3} = DSPEx.Client.request(messages, %{provider: :gemini})
# Local Ollama
{:ok, response4} = DSPEx.Client.request(messages, %{provider: :ollama})
[response1, response2, response3, response4]
end
# Structured outputs with ExLLM
def structured_output_example do
defmodule AnalysisSignature do
use DSPEx.Signature, "text -> sentiment, confidence, reasoning"
end
program = DSPEx.PredictStructured.new(AnalysisSignature, :anthropic)
{:ok, result} = DSPEx.Program.forward(program, %{
text: "I love programming in Elixir!"
})
# result => %{
# sentiment: "positive",
# confidence: "high",
# reasoning: "The text expresses clear enthusiasm..."
# }
end
# Session-based conversations
def session_example do
{:ok, client} = DSPEx.ClientManager.start_session(:anthropic)
{:ok, response1} = DSPEx.ClientManager.chat_with_session(
client,
"Hello, I'm learning Elixir"
)
{:ok, response2} = DSPEx.ClientManager.chat_with_session(
client,
"Can you explain GenServers?"
)
{:ok, history} = DSPEx.ClientManager.get_session_history(client)
{response1, response2, history}
end
# Provider capability checking
def capability_example do
# Check what's configured
configured_providers = Enum.filter(
[:openai, :anthropic, :gemini, :groq, :ollama],
&ExLLM.configured?/1
)
# Find providers with specific features
vision_providers = Enum.filter(
configured_providers,
fn provider -> ExLLM.provider_supports?(provider, :vision) end
)
# Get model recommendations
recommendations = ExLLM.recommend_models(
features: [:streaming, :function_calling],
max_cost_per_1k_tokens: 10.0
)
{configured_providers, vision_providers, recommendations}
end
end
Implementation Timeline
Week 1: Foundation
- Update dependencies (remove instructor_lite, add ex_llm)
- Define DSPEx.Client behaviour
- Create DSPEx.Clients.ExLLMClient
- Remove InstructorLite files
Week 2: Core Integration
- Update ClientManager with ExLLM sessions
- Create ExLLM structured output adapter
- Update PredictStructured without InstructorLite
- Basic integration testing
Week 3: Testing & Validation
- Update mock infrastructure for ExLLM
- Migrate existing tests to new client
- Add contract verification tests
- Performance testing
Week 4: Documentation & Polish
- Complete migration documentation
- Add usage examples
- Update configuration guides
- Final integration testing
Benefits Summary
🚀 Performance & Reliability
- 12+ provider support vs single provider (Gemini)
- Built-in retries with exponential backoff
- Automatic error recovery and circuit breakers
- Native streaming support with error recovery
🛠️ Developer Experience
- Unified API across all providers
- Built-in testing with mock provider
- Configuration flexibility (env vars, static, custom)
- Cost tracking and usage monitoring
🏗️ Architecture
- Behaviour-based design enables better testing
- Session management for conversation tracking
- Function calling support across providers
- Structured outputs without custom adapters
📈 Maintenance
- Reduced code - eliminate custom provider implementations
- Community support - leverage ExLLM ecosystem
- Future-proof - automatic new provider support
- Less complexity - remove custom error handling
This migration positions DSPEx as having best-in-class LLM client infrastructure while significantly reducing maintenance burden and increasing provider support.