# ServerChat.OpenRouter OpenRouter provider implementation for the ServerChat system. ## Overview [`ServerChat.OpenRouter`](../../lib/server_chat/openrouter.ex:1) implements the [`ServerChat.Provider`](../../lib/server_chat.ex:1) behaviour to integrate with OpenRouter's API, which provides access to multiple LLM models through an OpenAI-compatible interface. OpenRouter acts as a unified gateway to various AI models, allowing you to switch between providers (Anthropic, OpenAI, Google, etc.) without changing your code. ## Features - ✅ **OpenAI-Compatible API** - Uses standard OpenAI chat completions format - 🔧 **Function Calling** - Full support for tool use / function calling - 🔄 **Automatic Tool Execution** - Executes MCP tools and continues conversation - 🔁 **Recursive Tool Calling** - Handles multiple tool calls until completion - 📝 **Comprehensive Logging** - Emoji-enhanced logs for easy debugging - ⚡ **Multiple Models** - Access to 100+ models from various providers ## Configuration ### Environment Variables Set your OpenRouter API key: ```bash export OPENROUTER_API_KEY="sk-or-v1-..." ``` Optionally override the model: ```bash export OPENROUTER_MODEL="anthropic/claude-sonnet-4-5" ``` ### Application Config Configure in [`config/config.exs`](../../config/config.exs:1): ```elixir config :mcp, llm_provider: :openrouter, openrouter_model: "anthropic/claude-sonnet-4-5" ``` ### Getting an API Key 1. Visit [OpenRouter](https://openrouter.ai/) 2. Sign up for an account 3. Navigate to [API Keys](https://openrouter.ai/keys) 4. Create a new API key 5. Add credits to your account ## Available Models OpenRouter provides access to many models including: | Model ID | Provider | Description | |----------|----------|-------------| | `anthropic/claude-sonnet-4-5` | Anthropic | Claude Sonnet 4.5 - Balanced performance | | `anthropic/claude-opus-4` | Anthropic | Claude Opus 4 - Most capable | | `openai/gpt-4-turbo` | OpenAI | GPT-4 Turbo - Fast and capable | | `openai/gpt-4o` | OpenAI | GPT-4o - Multimodal | | `google/gemini-pro` | Google | Gemini Pro - Google's flagship | | `meta-llama/llama-3.1-405b` | Meta | Llama 3.1 405B - Open source | | `x-ai/grok-2` | xAI | Grok 2 - Real-time knowledge | See the full list at [OpenRouter Models](https://openrouter.ai/models). ## Usage ### Basic Chat ```elixir # Start a chat session {:ok, pid} = ServerChat.start_link(user_id: 1) # Switch to OpenRouter ServerChat.switch_provider(pid, :openrouter, "anthropic/claude-sonnet-4-5") # Send a message {:ok, reply} = ServerChat.chat(pid, "What's the weather in Tokyo?") IO.puts(reply) ``` ### Switching Models ```elixir # Switch to a different model (preserves history) ServerChat.set_model(pid, "openai/gpt-4-turbo") # Continue conversation with new model {:ok, reply} = ServerChat.chat(pid, "Tell me more") ``` ### With Tool Calling ```elixir # The LLM can automatically call MCP tools {:ok, reply} = ServerChat.chat(pid, "What's the weather in Tokyo and Paris?") # Behind the scenes: # 1. LLM requests to call get_weather tool twice # 2. Module executes both tools via MCP # 3. Results are sent back to LLM # 4. LLM synthesizes final response ``` ## Architecture ### Message Flow ``` User Message ↓ [format_tools/1] - Convert MCP tools to OpenAI format ↓ [call_llm/4] - POST to OpenRouter API ↓ [handle_response/5] - Process response ↓ ├─→ finish_reason: "stop" → Return final text │ └─→ finish_reason: "tool_calls" ↓ [handle_tool_calls/6] - Execute each tool ↓ [call_llm/4] - Call API again with results ↓ [handle_response/5] - Process new response (recursive) ``` ### Message Format OpenRouter uses the OpenAI message format: ```elixir # User message %{ role: "user", content: "What's the weather?" } # Assistant message (text only) %{ role: "assistant", content: "Let me check the weather for you." } # Assistant message (with tool calls) %{ role: "assistant", content: nil, tool_calls: [ %{ id: "call_abc123", type: "function", function: %{ name: "get_weather", arguments: "{\"location\": \"Tokyo\"}" } } ] } # Tool result message %{ role: "tool", tool_call_id: "call_abc123", content: "Temperature: 22°C, Sunny" } ``` ## API Reference ### [`format_tools/1`](../../lib/server_chat/openrouter.ex:13) Converts MCP tool definitions to OpenRouter's OpenAI-compatible format. **Parameters:** - `tools` - List of MCP tool maps **Returns:** - List of formatted tool definitions **Example:** ```elixir tools = [ %{ name: "get_weather", description: "Get weather for a location", input_schema: %{ type: "object", properties: %{ location: %{type: "string"} } } } ] formatted = ServerChat.OpenRouter.format_tools(tools) # [%{type: "function", function: %{name: "get_weather", ...}}] ``` ### [`call_llm/4`](../../lib/server_chat/openrouter.ex:27) Makes an HTTP POST request to OpenRouter's chat completions endpoint. **Parameters:** - `api_key` - OpenRouter API key - `tools` - Formatted tool definitions - `messages` - Conversation history - `model` - Model identifier **Returns:** - `{:ok, response_body}` - Success - `{:error, reason}` - Failure **Example:** ```elixir {:ok, response} = ServerChat.OpenRouter.call_llm( api_key, tools, messages, "anthropic/claude-sonnet-4-5" ) ``` ### [`handle_response/5`](../../lib/server_chat/openrouter.ex:55) Processes the API response and handles tool calls or final answers. **Parameters:** - `api_key` - OpenRouter API key - `tools` - Available tool definitions - `user_id` - User ID for MCP context - `response` - Parsed API response - `messages` - Current conversation history **Returns:** - `{:ok, text, updated_messages}` - Final response - `{:error, reason}` - Error **Finish Reasons:** - `"stop"` - Model completed naturally - `"tool_calls"` - Model wants to call tools - `"length"` - Response truncated (rare) - `"content_filter"` - Content filtered (rare) ## Tool Calling ### How It Works 1. **User sends message**: "What's the weather in Tokyo?" 2. **LLM responds with tool call**: ```elixir %{ "finish_reason" => "tool_calls", "message" => %{ "tool_calls" => [ %{ "id" => "call_abc123", "function" => %{ "name" => "get_weather", "arguments" => "{\"location\": \"Tokyo\"}" } } ] } } ``` 3. **Module executes tool**: ```elixir result = ServerChat.call_mcp_tool(user_id, "get_weather", %{"location" => "Tokyo"}) # "Temperature: 22°C, Sunny" ``` 4. **Tool result added to conversation**: ```elixir %{ role: "tool", tool_call_id: "call_abc123", content: "Temperature: 22°C, Sunny" } ``` 5. **LLM called again with result**: ```elixir %{ "finish_reason" => "stop", "message" => %{ "content" => "The weather in Tokyo is currently 22°C and sunny!" } } ``` ### Multiple Tool Calls The LLM can request multiple tools in a single response: ```elixir # User: "Compare weather in Tokyo and Paris" # LLM requests both tools: tool_calls = [ %{id: "call_1", function: %{name: "get_weather", arguments: "{\"location\": \"Tokyo\"}"}}, %{id: "call_2", function: %{name: "get_weather", arguments: "{\"location\": \"Paris\"}"}} ] # Both tools are executed # Results are sent back together # LLM synthesizes comparison ``` ## Error Handling The module handles various error conditions: ### API Errors ```elixir # Non-200 status code {:error, %{status: 401, body: %{"error" => "Invalid API key"}}} # Network failure {:error, %Req.TransportError{reason: :timeout}} ``` ### Response Errors ```elixir # Missing choices {:error, :no_choices} # Unexpected finish_reason {:error, :unexpected_response} ``` ### Tool Execution Errors ```elixir # JSON parse error for arguments # Logs: ⚠️ Failed to parse arguments: {invalid json} # Continues with empty map: %{} # MCP tool error # Returns error string in tool result: # "MCP Error: {:error, :tool_not_found}" ``` ## Logging The module uses emoji-enhanced logging for easy identification: | Emoji | Meaning | |-------|---------| | 💬 | User message | | 🤖 | Assistant response | | 🔧 | Tool call request | | 📡 | MCP tool result | | ⚠️ | Warning | | ❌ | Error | Example log output: ``` 💬 User: What's the weather in Tokyo? 🔧 OpenRouter wants to call: get_weather(%{"location" => "Tokyo"}) 📡 MCP Result: Temperature: 22°C, Sunny 🤖 OpenRouter: The weather in Tokyo is currently 22°C and sunny! ``` ## Comparison with Other Providers | Feature | OpenRouter | Anthropic | Perplexity | |---------|-----------|-----------|------------| | Multiple Models | ✅ 100+ | ❌ Claude only | ❌ Sonar only | | Tool Calling | ✅ Yes | ✅ Yes | ✅ Yes | | Citations | ❌ No | ❌ No | ✅ Yes | | Streaming | ✅ Yes* | ✅ Yes* | ✅ Yes* | | Cost | 💰 Varies | 💰💰 Higher | 💰 Lower | *Streaming not yet implemented in this module ## Best Practices ### Model Selection Choose models based on your needs: - **Speed**: `openai/gpt-4-turbo`, `anthropic/claude-sonnet-4-5` - **Quality**: `anthropic/claude-opus-4`, `openai/gpt-4o` - **Cost**: `meta-llama/llama-3.1-70b`, `google/gemini-pro` - **Specialized**: `x-ai/grok-2` (real-time), `perplexity/sonar` (search) ### Error Handling Always handle errors gracefully: ```elixir case ServerChat.chat(pid, message) do {:ok, reply} -> # Success IO.puts(reply) {:error, :chat_in_progress} -> # Another message is being processed IO.puts("Please wait...") {:error, reason} -> # API or tool error IO.puts("Error: #{inspect(reason)}") end ``` ### Tool Design Design tools to be: - **Idempotent** - Safe to call multiple times - **Fast** - LLM waits for results - **Descriptive** - Clear names and descriptions - **Validated** - Check arguments before execution ## Troubleshooting ### "Invalid API key" error ```bash # Check environment variable is set echo $OPENROUTER_API_KEY # Verify key is valid at https://openrouter.ai/keys ``` ### "Model not found" error ```bash # Check model ID is correct # See https://openrouter.ai/models for valid IDs # Example: Use "anthropic/claude-sonnet-4-5" not "claude-sonnet-4-5" ``` ### Tool calls not working ```elixir # Verify tools are properly formatted tools = ServerChat.OpenRouter.format_tools(raw_tools) IO.inspect(tools) # Check MCP server is running {:ok, %{tools: tools}} = MCPServer.Manager.list_tools(user_id) ``` ### Slow responses - Choose faster models (GPT-4 Turbo, Claude Sonnet) - Reduce conversation history length - Optimize tool execution time - Consider streaming (not yet implemented) ## Related Documentation - [`ServerChat`](server_chat.md) - Main chat system documentation - [`ServerChat.Anthropic`](../../lib/server_chat/anthropic.ex:1) - Anthropic provider - [`ServerChat.Perplexity`](../../lib/server_chat/perplexity.ex:1) - Perplexity provider - [OpenRouter API Docs](https://openrouter.ai/docs) - [OpenRouter Models](https://openrouter.ai/models) ## Source Code The complete implementation is in [`lib/server_chat/openrouter.ex`](../../lib/server_chat/openrouter.ex:1). --- *Last updated: 2026-02-04*