diff --git a/api/README.md b/api/README.md index 0065598d1..076e4eba6 100644 --- a/api/README.md +++ b/api/README.md @@ -30,6 +30,8 @@ OPENAI_API_KEY=your_openai_api_key # Required for embeddings and OpenAI m # Optional API Keys OPENROUTER_API_KEY=your_openrouter_api_key # Required only if using OpenRouter models +REQUESTY_API_KEY=your_requesty_api_key # Required only if using Requesty models +REQUESTY_BASE_URL=https://router.requesty.ai/v1 # Optional, use https://router.eu.requesty.ai/v1 for the EU region # AWS Bedrock Configuration AWS_ACCESS_KEY_ID=your_aws_access_key_id # Required for AWS Bedrock models @@ -53,6 +55,7 @@ If you're not using Ollama mode, you need to configure an OpenAI API key for emb > - Get a Google API key from [Google AI Studio](https://makersuite.google.com/app/apikey) > - Get an OpenAI API key from [OpenAI Platform](https://platform.openai.com/api-keys) > - Get an OpenRouter API key from [OpenRouter](https://openrouter.ai/keys) +> - Get a Requesty API key from [Requesty](https://app.requesty.ai/api-keys) > - Get AWS credentials from [AWS IAM Console](https://console.aws.amazon.com/iam/) #### Advanced Environment Configuration @@ -63,6 +66,7 @@ DeepWiki supports multiple LLM providers. The environment variables above are re - **Google Gemini**: Requires `GOOGLE_API_KEY` - **OpenAI**: Requires `OPENAI_API_KEY` - **OpenRouter**: Requires `OPENROUTER_API_KEY` +- **Requesty**: Requires `REQUESTY_API_KEY` - **AWS Bedrock**: Requires `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` - **Ollama**: No API key required (runs locally) @@ -83,7 +87,7 @@ DeepWiki now uses JSON configuration files to manage various system components i 1. **`generator.json`**: Configuration for text generation models - Located in `api/config/` by default - - Defines available model providers (Google, OpenAI, OpenRouter, AWS Bedrock, Ollama) + - Defines available model providers (Google, OpenAI, OpenRouter, Requesty, AWS Bedrock, Ollama) - Specifies default and available models for each provider - Contains model-specific parameters like temperature and top_p diff --git a/api/chat/_stream.py b/api/chat/_stream.py index dcb7f52ab..8084768a7 100644 --- a/api/chat/_stream.py +++ b/api/chat/_stream.py @@ -11,6 +11,7 @@ LITELLM_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, + REQUESTY_API_KEY, ) from api.logger import get_logger @@ -228,6 +229,27 @@ def _build_client(self): return LiteLLMClient() +class RequestyChatStreamer(_OpenAICompatStreamer): + provider = "requesty" + error_hint = ( + "Please check that you have set the REQUESTY_API_KEY " + "environment variable with a valid API key." + ) + + def __init__(self, *, model: str, model_config: MODEL_CFG): + if not REQUESTY_API_KEY: + logger.warning( + "REQUESTY_API_KEY not configured, but continuing with request" + ) + + super().__init__(model=model, model_config=model_config) + + def _build_client(self): + from api.clients import RequestyClient + + return RequestyClient() + + class BedrockChatStreamer(ChatStreamer): provider = "bedrock" error_hint = ( diff --git a/api/clients/__init__.py b/api/clients/__init__.py index d68b77cea..f837cb416 100644 --- a/api/clients/__init__.py +++ b/api/clients/__init__.py @@ -15,6 +15,7 @@ from .litellm import LiteLLMClient from .ollama import OllamaClient from .openrouter import OpenRouterClient +from .requesty import RequestyClient __all__ = [ "AnthropicBedrockClient", @@ -27,4 +28,5 @@ "OllamaClient", "OpenAIClient", "OpenRouterClient", + "RequestyClient", ] diff --git a/api/clients/requesty.py b/api/clients/requesty.py new file mode 100644 index 000000000..ba9ff6a44 --- /dev/null +++ b/api/clients/requesty.py @@ -0,0 +1,101 @@ +"""Requesty ModelClient integration.""" + +import os +from typing import Any, Dict, Literal, Optional + +from adalflow.components.model_client.openai_client import OpenAIClient +from adalflow.core.types import ModelType +from openai import AsyncOpenAI, OpenAI + +REQUESTY_DEFAULT_HEADERS = { + "HTTP-Referer": "https://github.com/AsyncFuncAI/deepwiki-open", + "X-Title": "DeepWiki", +} + + +class RequestyClient(OpenAIClient): + """ + Requesty OpenAI-compatible client. + + Requesty is an LLM gateway that gives access to hundreds of models from + many providers through a single OpenAI-compatible endpoint, so we can + reuse almost all OpenAIClient behavior while overriding only the client + initialization. + + Visit https://docs.requesty.ai for more details. + + Expected environment variables: + + REQUESTY_API_KEY=your_requesty_api_key + REQUESTY_BASE_URL=https://router.requesty.ai/v1 (optional, use https://router.eu.requesty.ai/v1 for EU) + + Example model names: + openai/gpt-4o-mini + anthropic/claude-sonnet-4-5 + google/gemini-2.5-flash + """ + + def __init__( + self, + api_key: Optional[str] = None, + input_type: Literal["text", "messages"] = "text", + base_url: Optional[str] = None, + env_api_key_name: str = "REQUESTY_API_KEY", + ): + resolved_base_url = base_url or os.getenv( + "REQUESTY_BASE_URL", "https://router.requesty.ai/v1" + ) + super().__init__( + api_key=api_key, + input_type=input_type, + base_url=resolved_base_url, + env_api_key_name=env_api_key_name, + ) + + def _resolve_api_key(self) -> str: + api_key = self._api_key or os.getenv(self._env_api_key_name) + if not api_key: + raise ValueError( + f"Environment variable {self._env_api_key_name} must be set" + ) + return api_key + + def init_sync_client(self): + """Initialize synchronous Requesty client.""" + return OpenAI( + api_key=self._resolve_api_key(), + base_url=self.base_url, + default_headers=REQUESTY_DEFAULT_HEADERS, + ) + + def init_async_client(self): + """Initialize asynchronous Requesty client.""" + return AsyncOpenAI( + api_key=self._resolve_api_key(), + base_url=self.base_url, + default_headers=REQUESTY_DEFAULT_HEADERS, + ) + + def convert_inputs_to_api_kwargs( + self, + input: Optional[Any] = None, + model_kwargs: Optional[Dict] = None, + model_type: ModelType = ModelType.UNDEFINED, + ) -> Dict: + """Send a plain text prompt as a user message, like the OpenRouter client. + + OpenAIClient sends a plain text prompt as a single system message, which + Anthropic and Gemini models reject. + """ + api_kwargs = super().convert_inputs_to_api_kwargs( + input=input, model_kwargs=model_kwargs or {}, model_type=model_type + ) + messages = api_kwargs.get("messages") + if ( + model_type == ModelType.LLM + and messages + and len(messages) == 1 + and messages[0].get("role") == "system" + ): + messages[0] = {"role": "user", "content": messages[0]["content"]} + return api_kwargs diff --git a/api/config.py b/api/config.py index ab9877271..77ae91abe 100644 --- a/api/config.py +++ b/api/config.py @@ -15,6 +15,7 @@ OllamaClient, OpenAIClient, OpenRouterClient, + RequestyClient, ) from api.logger import get_logger @@ -30,6 +31,7 @@ LITELLM_API_KEY = os.environ.get("LITELLM_API_KEY") GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY") OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY") +REQUESTY_API_KEY = os.environ.get("REQUESTY_API_KEY") AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") AWS_SESSION_TOKEN = os.environ.get("AWS_SESSION_TOKEN") @@ -45,6 +47,8 @@ os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY if OPENROUTER_API_KEY: os.environ["OPENROUTER_API_KEY"] = OPENROUTER_API_KEY +if REQUESTY_API_KEY: + os.environ["REQUESTY_API_KEY"] = REQUESTY_API_KEY if AWS_ACCESS_KEY_ID: os.environ["AWS_ACCESS_KEY_ID"] = AWS_ACCESS_KEY_ID if AWS_SECRET_ACCESS_KEY: @@ -74,6 +78,7 @@ OpenAIClient.__name__: OpenAIClient, LiteLLMClient.__name__: LiteLLMClient, OpenRouterClient.__name__: OpenRouterClient, + RequestyClient.__name__: RequestyClient, OllamaClient.__name__: OllamaClient, BedrockClient.__name__: BedrockClient, AzureAIClient.__name__: AzureAIClient, @@ -87,6 +92,7 @@ "openai": OpenAIClient, "litellm": LiteLLMClient, "openrouter": OpenRouterClient, + "requesty": RequestyClient, "ollama": OllamaClient, "bedrock": BedrockClient, "azure": AzureAIClient, diff --git a/api/config/generator.json b/api/config/generator.json index f88179098..d89cd2376 100644 --- a/api/config/generator.json +++ b/api/config/generator.json @@ -113,6 +113,34 @@ } } }, + "requesty": { + "default_model": "openai/gpt-4o-mini", + "supportsCustomModel": true, + "models": { + "openai/gpt-4o-mini": { + "temperature": 0.7, + "top_p": 0.8 + }, + "openai/gpt-4.1": { + "temperature": 0.7, + "top_p": 0.8 + }, + "openai/gpt-5-mini": { + "temperature": 1.0 + }, + "anthropic/claude-sonnet-4-5": { + "temperature": 0.7 + }, + "google/gemini-2.5-flash": { + "temperature": 1.0, + "top_p": 0.8 + }, + "deepseek/deepseek-chat": { + "temperature": 0.7, + "top_p": 0.8 + } + } + }, "ollama": { "default_model": "qwen3:1.7b", "supportsCustomModel": true, diff --git a/api/config/generator.litellm.json b/api/config/generator.litellm.json index 9d28054b7..b27c4e3b1 100644 --- a/api/config/generator.litellm.json +++ b/api/config/generator.litellm.json @@ -131,6 +131,34 @@ } } }, + "requesty": { + "default_model": "openai/gpt-4o-mini", + "supportsCustomModel": true, + "models": { + "openai/gpt-4o-mini": { + "temperature": 0.7, + "top_p": 0.8 + }, + "openai/gpt-4.1": { + "temperature": 0.7, + "top_p": 0.8 + }, + "openai/gpt-5-mini": { + "temperature": 1.0 + }, + "anthropic/claude-sonnet-4-5": { + "temperature": 0.7 + }, + "google/gemini-2.5-flash": { + "temperature": 1.0, + "top_p": 0.8 + }, + "deepseek/deepseek-chat": { + "temperature": 0.7, + "top_p": 0.8 + } + } + }, "ollama": { "default_model": "qwen3:1.7b", "supportsCustomModel": true, diff --git a/src/messages/en.json b/src/messages/en.json index d42edc7d4..2c54ec5db 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -45,6 +45,7 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerRequesty": "Requesty", "providerOllama": "Ollama (Local)", "localOllama": "Local Ollama Model", "experimental": "Experimental", diff --git a/src/messages/es.json b/src/messages/es.json index d2fb5282a..0d1f693d9 100644 --- a/src/messages/es.json +++ b/src/messages/es.json @@ -45,6 +45,7 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerRequesty": "Requesty", "providerOllama": "Ollama (Local)", "localOllama": "Modelo Ollama Local", "experimental": "Experimental", diff --git a/src/messages/fr.json b/src/messages/fr.json index a4a528393..2335c6862 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -45,6 +45,7 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerRequesty": "Requesty", "providerOllama": "Ollama (Local)", "localOllama": "Modèle Ollama local", "experimental": "Expérimental", diff --git a/src/messages/ja.json b/src/messages/ja.json index 28ea9460a..9b8708f84 100644 --- a/src/messages/ja.json +++ b/src/messages/ja.json @@ -45,6 +45,7 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerRequesty": "Requesty", "providerOllama": "Ollama(ローカル)", "localOllama": "ローカルOllamaモデル", "experimental": "実験的", diff --git a/src/messages/kr.json b/src/messages/kr.json index 1b23f645e..3a9c478f3 100644 --- a/src/messages/kr.json +++ b/src/messages/kr.json @@ -45,6 +45,7 @@ "providerGoogle": "구글", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerRequesty": "Requesty", "providerOllama": "Ollama (로컬)", "localOllama": "로컬 Ollama 모델", "experimental": "실험적", diff --git a/src/messages/pt-br.json b/src/messages/pt-br.json index 0d9699456..295b1373b 100644 --- a/src/messages/pt-br.json +++ b/src/messages/pt-br.json @@ -45,6 +45,7 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerRequesty": "Requesty", "providerOllama": "Ollama (Local)", "localOllama": "Modelo Ollama Local", "experimental": "Experimental", diff --git a/src/messages/ru.json b/src/messages/ru.json index 17823a8bd..67449ae8c 100644 --- a/src/messages/ru.json +++ b/src/messages/ru.json @@ -45,6 +45,7 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerRequesty": "Requesty", "providerOllama": "Ollama (локально)", "localOllama": "Локальная модель Ollama", "experimental": "Экспериментально", diff --git a/src/messages/vi.json b/src/messages/vi.json index 4d43d955e..1dcfde198 100644 --- a/src/messages/vi.json +++ b/src/messages/vi.json @@ -45,6 +45,7 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerRequesty": "Requesty", "providerOllama": "Ollama (Cục bộ)", "localOllama": "Mô hình Ollama cục bộ", "experimental": "Thử nghiệm", diff --git a/src/messages/zh-tw.json b/src/messages/zh-tw.json index 8c18f9af5..81d107aa0 100644 --- a/src/messages/zh-tw.json +++ b/src/messages/zh-tw.json @@ -45,6 +45,7 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerRequesty": "Requesty", "providerOllama": "Ollama(本機)", "localOllama": "本機 Ollama 模型", "experimental": "實驗性", diff --git a/src/messages/zh.json b/src/messages/zh.json index b0b60bea3..8af1a7259 100644 --- a/src/messages/zh.json +++ b/src/messages/zh.json @@ -45,6 +45,7 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerRequesty": "Requesty", "providerOllama": "Ollama (本地)", "localOllama": "本地Ollama模型", "experimental": "实验性", diff --git a/tests/unit/test_chat.py b/tests/unit/test_chat.py index 323d24131..d038630e5 100644 --- a/tests/unit/test_chat.py +++ b/tests/unit/test_chat.py @@ -11,6 +11,7 @@ OllamaChatStreamer, OpenAIChatStreamer, OpenRouterChatStreamer, + RequestyChatStreamer, ) @@ -19,6 +20,7 @@ [ ("ollama", OllamaChatStreamer), ("openrouter", OpenRouterChatStreamer), + ("requesty", RequestyChatStreamer), ("openai", OpenAIChatStreamer), ("azure", AzureChatStreamer), ("bedrock", BedrockChatStreamer), @@ -37,6 +39,7 @@ def test_every_provider_is_registered(provider, expected): [ ("ollama", OllamaChatStreamer), ("openrouter", OpenRouterChatStreamer), + ("requesty", RequestyChatStreamer), ("openai", OpenAIChatStreamer), ("azure", AzureChatStreamer), ("bedrock", BedrockChatStreamer), diff --git a/tests/unit/test_requesty_client.py b/tests/unit/test_requesty_client.py new file mode 100644 index 000000000..a6b73c54f --- /dev/null +++ b/tests/unit/test_requesty_client.py @@ -0,0 +1,30 @@ +from adalflow.core.types import ModelType + +from api.clients import RequestyClient + + +def test_requesty_client_uses_default_base_url(monkeypatch): + monkeypatch.delenv("REQUESTY_BASE_URL", raising=False) + monkeypatch.setenv("REQUESTY_API_KEY", "test-key") + client = RequestyClient() + assert client.base_url == "https://router.requesty.ai/v1" + assert str(client.sync_client.base_url).startswith("https://router.requesty.ai/v1") + + +def test_requesty_client_base_url_override(monkeypatch): + monkeypatch.setenv("REQUESTY_BASE_URL", "https://router.eu.requesty.ai/v1") + monkeypatch.setenv("REQUESTY_API_KEY", "test-key") + client = RequestyClient() + assert client.base_url == "https://router.eu.requesty.ai/v1" + + +def test_requesty_client_sends_text_prompt_as_user_message(monkeypatch): + monkeypatch.setenv("REQUESTY_API_KEY", "test-key") + client = RequestyClient() + api_kwargs = client.convert_inputs_to_api_kwargs( + input="hello", + model_kwargs={"model": "openai/gpt-4o-mini"}, + model_type=ModelType.LLM, + ) + assert api_kwargs["messages"] == [{"role": "user", "content": "hello"}] + assert api_kwargs["model"] == "openai/gpt-4o-mini"