Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .mistral_client import MistralAI, ChatMistralAI, MistralAgent, ChatMistralAgent
from .gemini_client import ChatGemini, GeminiAgent
from .groq_client import ChatGroq, GroqAgent
from .openrouter_client import ChatOpenRouter
from .openrouter_client import ChatOpenRouter, OpenRouterAgent, ChatOpenRouterAgent

__all__ = [
"BaseChatModel",
Expand All @@ -19,7 +19,9 @@
"GeminiAgent",
"ChatGroq",
"GroqAgent",
"ChatOpenRouter"
"ChatOpenRouter",
"OpenRouterAgent",
"ChatOpenRouterAgent"
]

"""Parameters for Each Function Call
Expand Down
118 changes: 116 additions & 2 deletions app/providers/openrouter_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from app.Errors import *
from typing import Any, Dict, List, Optional
from langchain_core.messages import AIMessage
from langchain_core.tools import tool, StructuredTool
import warnings
from app.providers.base import BaseChatModel, BaseAgentModel
from app.defaults import DEFAULT_OPENROUTER_MODEL, MAX_TOKENS, TEMPERATURE
Expand Down Expand Up @@ -50,8 +51,8 @@ def __init__(
model : str = DEFAULT_OPENROUTER_MODEL,
temperature : float = TEMPERATURE,
max_tokens : int = MAX_TOKENS,
app_title : str = "RSRoute Server",
app_url : str = "https://www.github.com/ItzRustam/rsroute",
app_title : str = "RSRoute",
app_url : str = "https://www.rsroute.freedev.app/",
app_categories : List[str] = None,
max_retries : int = 2,

Expand Down Expand Up @@ -95,3 +96,116 @@ def __init__(
# Backwards compatibility attributes
self.__model = self._model
self.__params = self._params


class OpenRouterAgent(BaseAgentModel):
"""OpenRouter-powered agentic wrapper with tool-binding support via Langchain.

Wraps `ChatOpenRouter` and binds fake/stub tools to the model,
enabling the model to reason about and invoke external tools by name.

Attributes:
_model: Instance of Langchain's `ChatOpenRouter` with tools bound.
_params (Dict[str, Any]): Dictionary containing the parameters used to initialize
the model, useful for debugging or introspection.

Raises:
InvalidRequest: If `api_key` is not provided during initialization.
InvalidRequest: If `tools_info` is not provided, empty, or incorrectly formatted.

Example:
```python
tools = [{"name": "web_search", "description": "Search the web"}]
agent = OpenRouterAgent(
api_key="sk-or-v1-FAKE_KEY",
tools_info=tools
)
response = agent("What is the weather today?")
```
"""

def __fake_tool(self, name : str, description : str) -> StructuredTool:
return self._fake_tool(name=name, description=description)

def __create_fake_tools(self, tools_info : List[Dict[str, str]]) -> List[StructuredTool]:
return self._create_fake_tools(tools_info=tools_info)

def __init__(
self,
api_key : str = None,
model : str = DEFAULT_OPENROUTER_MODEL,
temperature : float = TEMPERATURE,
max_tokens : int = MAX_TOKENS,
app_title : str = "RSRoute",
app_url : str = "https://www.rsroute.freedev.app/",
app_categories : List[str] = None,
max_retries : int = 2,

# provider preferences
providers : Dict[Any, Any] = None,

# Base url
end_point : str = None,

tools_info : List[Dict[str, str]] = None,
**model_kwargs
) -> AIMessage:
warnings.warn("For Best Agentic Experience use rsroute python package. https://www.github.com/ItzRustam/rsroute-package")
super().__init__()
self._validate_api_key(api_key=api_key)
self._validate_tools_info(tools_info)

created_tools = self._create_fake_tools(tools_info=tools_info)

# Parameter Holding
self._params : Dict[str, Any] = {
"model" : model,
"temperature" : temperature,
"max_tokens" : max_tokens,
"app_url" : app_url,
"app_title" : app_title,
"app_categories" : app_categories,
"max_retries" : max_retries,
"openrouter_provider" : providers,
"end_point" : end_point,
"binded_tools" : tools_info,
"model_kwargs" : model_kwargs
}

# Hidden Agent Model
self._model = _LangchainChatOpenRouter(
model=model,
api_key=api_key,
temperature=temperature,
max_tokens=max_tokens,
base_url=end_point,
app_title=app_title,
app_url=app_url,
app_categories=app_categories,
max_retries=max_retries,
openrouter_provider=providers,
model_kwargs=model_kwargs
).bind_tools(tools=created_tools)

self.__agent = self._model
self.__params = self._params

def __call__(self, query : Any = None):
"""Generate response on given `query`. Same as `generate()`."""
return self.generate(query=query)

def generate(self, query : Any = None):
"""Generate response on given `query`."""
return super().generate(query=query)

def get_params(self):
"""Model initialized parameters."""
return super().get_params()


ChatOpenRouterAgent = OpenRouterAgent


if __name__ == "__main__":
llm = ChatOpenRouter(model="openai/gpt-4o-mini", api_key="sk-or-v1-fake_key")
response = llm(query="Hello? How can you help me?")
1 change: 1 addition & 0 deletions app/server/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def root():
},
"NOTE": "use exact `master_key` given in `.env` or else it won't work. don't forget to put API key which model you wanna use.",
"GitHub": "https://www.github.com/ItzRustam/rsroute",
"rsroute": "https://www.rsroute.freedev.app/",
"By": "ItzRustam (Rustam Bhadouriya)",
"version": "v1-starter-version",
}
Expand Down
188 changes: 185 additions & 3 deletions test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
from tests import auth_test
from tests import get_logger
from tests import api_config_test
from tests import test_mistral
from tests import test_mistral, test_mistral_agent
from tests import test_gemini, test_gemini_agent
from tests import test_groq
from tests import test_groq, test_groq_agent
from tests import test_openrouter, test_openrouter_agent
from tests import (
test_base_estimator_inheritance,
test_base_estimator_methods,
Expand All @@ -29,6 +30,48 @@
case_tools_missing_name,
case_tools_empty_dict_in_list,
)
from tests.groq_agent_test import (
case_tools_none as groq_case_tools_none,
case_tools_empty_list as groq_case_tools_empty_list,
case_tools_wrong_type_string as groq_case_tools_wrong_type_string,
case_tools_wrong_type_int as groq_case_tools_wrong_type_int,
case_tools_wrong_type_dict as groq_case_tools_wrong_type_dict,
case_tools_missing_description as groq_case_tools_missing_description,
case_tools_missing_name as groq_case_tools_missing_name,
case_tools_empty_dict_in_list as groq_case_tools_empty_dict_in_list,
case_api_key_none as groq_case_api_key_none,
case_valid_initialization as groq_case_valid_initialization,
case_agent_inheritance as groq_case_agent_inheritance,
case_agent_methods as groq_case_agent_methods,
)
from tests.mistral_agent_test import (
case_tools_none as mistral_case_tools_none,
case_tools_empty_list as mistral_case_tools_empty_list,
case_tools_wrong_type_string as mistral_case_tools_wrong_type_string,
case_tools_wrong_type_int as mistral_case_tools_wrong_type_int,
case_tools_wrong_type_dict as mistral_case_tools_wrong_type_dict,
case_tools_missing_description as mistral_case_tools_missing_description,
case_tools_missing_name as mistral_case_tools_missing_name,
case_tools_empty_dict_in_list as mistral_case_tools_empty_dict_in_list,
case_api_key_none as mistral_case_api_key_none,
case_valid_initialization as mistral_case_valid_initialization,
case_agent_inheritance as mistral_case_agent_inheritance,
case_agent_methods as mistral_case_agent_methods,
)
from tests.openrouter_agent_test import (
case_tools_none as or_case_tools_none,
case_tools_empty_list as or_case_tools_empty_list,
case_tools_wrong_type_string as or_case_tools_wrong_type_string,
case_tools_wrong_type_int as or_case_tools_wrong_type_int,
case_tools_wrong_type_dict as or_case_tools_wrong_type_dict,
case_tools_missing_description as or_case_tools_missing_description,
case_tools_missing_name as or_case_tools_missing_name,
case_tools_empty_dict_in_list as or_case_tools_empty_dict_in_list,
case_api_key_none as or_case_api_key_none,
case_valid_initialization as or_case_valid_initialization,
case_agent_inheritance as or_case_agent_inheritance,
case_agent_methods as or_case_agent_methods,
)
from dotenv import load_dotenv
load_dotenv()

Expand Down Expand Up @@ -283,12 +326,147 @@ def test_gemini_agent():
return True


def test_groq_agent_cases():
print("=========== Running GroqAgent tools_info TestCases ===========")

cases = [
("TestCase #1 - tools_info=None", groq_case_tools_none),
("TestCase #2 - tools_info=[] (empty list)", groq_case_tools_empty_list),
("TestCase #3 - tools_info='search' (string)", groq_case_tools_wrong_type_string),
("TestCase #4 - tools_info=42 (int)", groq_case_tools_wrong_type_int),
("TestCase #5 - tools_info={} (bare dict)", groq_case_tools_wrong_type_dict),
("TestCase #6 - missing 'description' key", groq_case_tools_missing_description),
("TestCase #7 - missing 'name' key", groq_case_tools_missing_name),
("TestCase #8 - [{}] empty dict in list", groq_case_tools_empty_dict_in_list),
("TestCase #9 - api_key=None", groq_case_api_key_none),
("TestCase #10 - Valid initialization & params", groq_case_valid_initialization),
("TestCase #11 - GroqAgent inheritance", groq_case_agent_inheritance),
("TestCase #12 - GroqAgent methods", groq_case_agent_methods),
]

passed = 0
for label, case_fn in cases:
logger.info(label)
val, result = case_fn()
logger.debug(f"Result: {result}")
if val:
passed += 1
else:
logger.error(f"{label} — FAILED: {result}")
return False

print(f"=========== All Test Cases Passed {passed}/{len(cases)} ===========")
return True


def test_mistral_agent_cases():
print("=========== Running MistralAgent tools_info TestCases ===========")

cases = [
("TestCase #1 - tools_info=None", mistral_case_tools_none),
("TestCase #2 - tools_info=[] (empty list)", mistral_case_tools_empty_list),
("TestCase #3 - tools_info='search' (string)", mistral_case_tools_wrong_type_string),
("TestCase #4 - tools_info=42 (int)", mistral_case_tools_wrong_type_int),
("TestCase #5 - tools_info={} (bare dict)", mistral_case_tools_wrong_type_dict),
("TestCase #6 - missing 'description' key", mistral_case_tools_missing_description),
("TestCase #7 - missing 'name' key", mistral_case_tools_missing_name),
("TestCase #8 - [{}] empty dict in list", mistral_case_tools_empty_dict_in_list),
("TestCase #9 - api_key=None", mistral_case_api_key_none),
("TestCase #10 - Valid initialization & params", mistral_case_valid_initialization),
("TestCase #11 - MistralAgent inheritance", mistral_case_agent_inheritance),
("TestCase #12 - MistralAgent methods", mistral_case_agent_methods),
]

passed = 0
for label, case_fn in cases:
logger.info(label)
val, result = case_fn()
logger.debug(f"Result: {result}")
if val:
passed += 1
else:
logger.error(f"{label} — FAILED: {result}")
return False

print(f"=========== All Test Cases Passed {passed}/{len(cases)} ===========")
return True


def openrouter_testing():
FAKE_KEY = "sk-or-v1-fake_openrouter_key"
print("=========== Running ChatOpenRouter TestCases with Fake API ===========")

logger.info("TestCase #1 - KEY & query both None.")
val, result = test_openrouter()
logger.debug(f"case #1 result - {result}")
if not val:
logger.error("TestCase #1 Failed.")
return False

logger.info("TestCase #2 - query = None")
val, result = test_openrouter(KEY=FAKE_KEY)
logger.debug(f"case #2 result - {result}")
if not val:
logger.error("TestCase #2 Failed.")
return False

logger.info("TestCase #3 - valid value with real_api=False")
val, result = test_openrouter(KEY=FAKE_KEY, query="hey", real_api=False)
logger.debug(f"case #3 result - {result}")
if not val:
logger.error("TestCase #3 Failed.")
return False

logger.info("TestCase #4 - valid value with real_api=True with fake api")
val, result = test_openrouter(KEY=FAKE_KEY, query="hey", real_api=True)
logger.debug(f"case #4 result - {result}")
if val != False:
logger.error("TestCase #4 Failed.")
return False

print("=========== All Test Case Passed 4/4 ===========")
return True


def test_openrouter_agent_cases():
print("=========== Running OpenRouterAgent tools_info TestCases ===========")

cases = [
("TestCase #1 - tools_info=None", or_case_tools_none),
("TestCase #2 - tools_info=[] (empty list)", or_case_tools_empty_list),
("TestCase #3 - tools_info='search' (string)", or_case_tools_wrong_type_string),
("TestCase #4 - tools_info=42 (int)", or_case_tools_wrong_type_int),
("TestCase #5 - tools_info={} (bare dict)", or_case_tools_wrong_type_dict),
("TestCase #6 - missing 'description' key", or_case_tools_missing_description),
("TestCase #7 - missing 'name' key", or_case_tools_missing_name),
("TestCase #8 - [{}] empty dict in list", or_case_tools_empty_dict_in_list),
("TestCase #9 - api_key=None", or_case_api_key_none),
("TestCase #10 - Valid initialization & params", or_case_valid_initialization),
("TestCase #11 - OpenRouterAgent inheritance", or_case_agent_inheritance),
("TestCase #12 - OpenRouterAgent methods", or_case_agent_methods),
]

passed = 0
for label, case_fn in cases:
logger.info(label)
val, result = case_fn()
logger.debug(f"Result: {result}")
if val:
passed += 1
else:
logger.error(f"{label} — FAILED: {result}")
return False

print(f"=========== All Test Cases Passed {passed}/{len(cases)} ===========")
return True


def base_estimator_testing():
print("=========== Running BaseChatModel, Agent Classes & Endpoints TestCases ===========")
cases = [
("TestCase #1 - BaseChatModel & BaseAgentModel inheritance", test_base_estimator_inheritance),
("TestCase #2 - BaseChatModel validation & parameter methods", test_base_estimator_methods),
("TestCase #3 - Agent classes tools_info validation (Gemini, Groq, Mistral)", test_agent_classes_validation),
("TestCase #3 - Agent classes tools_info validation (Gemini, Groq, Mistral, OpenRouter)", test_agent_classes_validation),
("TestCase #4 - DDGS web_search tool check", test_ddgs_web_search),
("TestCase #5 - Groq chat & parameters endpoints (/v1/groq/chat, /v1/groq/parameters)", test_groq_endpoints),
("TestCase #6 - Agent endpoints (/v1/gemini/agent, /v1/groq/agent, /v1/mistral/agent)", test_agent_endpoints),
Expand Down Expand Up @@ -316,7 +494,11 @@ def main():
results.append(mistral_testing()) # Mistral provider testing
results.append(gemini_testing()) # Gemini Testing
results.append(groq_testing()) # Groq Testing
results.append(openrouter_testing()) # OpenRouter Testing
results.append(test_gemini_agent()) # GeminiAgent tools_info Testing
results.append(test_groq_agent_cases()) # GroqAgent tools_info Testing
results.append(test_mistral_agent_cases()) # MistralAgent tools_info Testing
results.append(test_openrouter_agent_cases()) # OpenRouterAgent tools_info Testing
results.append(base_estimator_testing()) # BaseChatModel & Groq endpoints Testing

return all(results) # Return False if any Test Case Failed
Expand Down
Loading
Loading