diff --git a/api/README.md b/api/README.md index 0065598d1..3075afc2a 100644 --- a/api/README.md +++ b/api/README.md @@ -30,6 +30,7 @@ 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 +ORCAROUTER_API_KEY=your_orcarouter_api_key # Required only if using OrcaRouter models # AWS Bedrock Configuration AWS_ACCESS_KEY_ID=your_aws_access_key_id # Required for AWS Bedrock models @@ -53,6 +54,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 an OrcaRouter API key from [OrcaRouter](https://www.orcarouter.ai) > - Get AWS credentials from [AWS IAM Console](https://console.aws.amazon.com/iam/) #### Advanced Environment Configuration @@ -63,6 +65,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` +- **OrcaRouter**: Requires `ORCAROUTER_API_KEY` - **AWS Bedrock**: Requires `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` - **Ollama**: No API key required (runs locally) @@ -83,7 +86,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, OrcaRouter, 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..226c6bbba 100644 --- a/api/chat/_stream.py +++ b/api/chat/_stream.py @@ -11,6 +11,7 @@ LITELLM_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, + ORCAROUTER_API_KEY, ) from api.logger import get_logger @@ -228,6 +229,28 @@ def _build_client(self): return LiteLLMClient() +class OrcaRouterChatStreamer(_OpenAICompatStreamer): + provider = "orcarouter" + error_hint = ( + "Please check that you have set the ORCAROUTER_API_KEY " + "environment variable with a valid API key." + ) + + def __init__(self, *, model: str, model_config: MODEL_CFG): + if not ORCAROUTER_API_KEY: + logger.warning( + "ORCAROUTER_API_KEY not configured, but continuing with request" + ) + # We'll let the OrcaRouterClient handle this and return an error message + + super().__init__(model=model, model_config=model_config) + + def _build_client(self): + from api.clients import OrcaRouterClient + + return OrcaRouterClient() + + class BedrockChatStreamer(ChatStreamer): provider = "bedrock" error_hint = ( diff --git a/api/clients/__init__.py b/api/clients/__init__.py index d68b77cea..b610b3aec 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 .orcarouter import OrcaRouterClient __all__ = [ "AnthropicBedrockClient", @@ -27,4 +28,5 @@ "OllamaClient", "OpenAIClient", "OpenRouterClient", + "OrcaRouterClient", ] diff --git a/api/clients/orcarouter.py b/api/clients/orcarouter.py new file mode 100644 index 000000000..fa1d48141 --- /dev/null +++ b/api/clients/orcarouter.py @@ -0,0 +1,62 @@ +"""OrcaRouter OpenAI-compatible client.""" + +import os +from typing import Literal, Optional + +from adalflow.components.model_client.openai_client import OpenAIClient +from openai import AsyncOpenAI, OpenAI + + +class OrcaRouterClient(OpenAIClient): + """ + OrcaRouter OpenAI-compatible client. + + OrcaRouter is an OpenAI-compatible routing gateway that fronts multiple + upstream providers behind namespaced model ids (e.g. ``openai/gpt-5.5``). + It exposes an OpenAI-compatible API surface, so we can reuse almost all + OpenAIClient behavior while overriding only the client initialization. + + Expected environment variables: + + ORCAROUTER_API_KEY=sk-orca-... + + Example model names: + openai/gpt-5.5 + openai/gpt-4o + anthropic/claude-sonnet-4.5 + """ + + def __init__( + self, + api_key: Optional[str] = None, + input_type: Literal["text", "messages"] = "text", + base_url: Optional[str] = None, + env_api_key_name: str = "ORCAROUTER_API_KEY", + ): + resolved_base_url = base_url or os.getenv( + "ORCAROUTER_BASE_URL", "https://api.orcarouter.ai/v1" + ) + if not resolved_base_url.endswith("/v1"): + resolved_base_url = f"{resolved_base_url.rstrip('/')}/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 init_sync_client(self): + """Initialize synchronous OrcaRouter OpenAI-compatible client.""" + api_key = self._api_key or os.getenv(self._env_api_key_name, "dummy") + return OpenAI( + api_key=api_key, + base_url=self.base_url, + ) + + def init_async_client(self): + """Initialize asynchronous OrcaRouter OpenAI-compatible client.""" + api_key = self._api_key or os.getenv(self._env_api_key_name, "dummy") + return AsyncOpenAI( + api_key=api_key, + base_url=self.base_url, + ) diff --git a/api/config.py b/api/config.py index ab9877271..970b8f62a 100644 --- a/api/config.py +++ b/api/config.py @@ -15,6 +15,7 @@ OllamaClient, OpenAIClient, OpenRouterClient, + OrcaRouterClient, ) 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") +ORCAROUTER_API_KEY = os.environ.get("ORCAROUTER_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 ORCAROUTER_API_KEY: + os.environ["ORCAROUTER_API_KEY"] = ORCAROUTER_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, + OrcaRouterClient.__name__: OrcaRouterClient, OllamaClient.__name__: OllamaClient, BedrockClient.__name__: BedrockClient, AzureAIClient.__name__: AzureAIClient, @@ -87,6 +92,7 @@ "openai": OpenAIClient, "litellm": LiteLLMClient, "openrouter": OpenRouterClient, + "orcarouter": OrcaRouterClient, "ollama": OllamaClient, "bedrock": BedrockClient, "azure": AzureAIClient, diff --git a/api/config/generator.json b/api/config/generator.json index f88179098..4498eaae5 100644 --- a/api/config/generator.json +++ b/api/config/generator.json @@ -113,6 +113,34 @@ } } }, + "orcarouter": { + "default_model": "openai/gpt-5.5", + "supportsCustomModel": true, + "models": { + "openai/gpt-5.5": { + "temperature": 0.7, + "top_p": 0.8 + }, + "openai/gpt-5-nano": { + "temperature": 1.0 + }, + "openai/gpt-5-mini": { + "temperature": 1.0 + }, + "openai/gpt-4o": { + "temperature": 0.7, + "top_p": 0.8 + }, + "openai/gpt-4.1": { + "temperature": 0.7, + "top_p": 0.8 + }, + "anthropic/claude-sonnet-4.5": { + "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..4ec53c375 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -45,11 +45,14 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerOrcaRouter": "OrcaRouter", "providerOllama": "Ollama (Local)", "localOllama": "Local Ollama Model", "experimental": "Experimental", "useOpenRouter": "Use OpenRouter API", + "useOrcaRouter": "Use OrcaRouter API", "openRouterModel": "OpenRouter Model", + "orcaRouterModel": "OrcaRouter Model", "useOpenai": "Use Openai API", "openaiModel": "Openai Model", "useCustomModel": "Use custom model", diff --git a/src/messages/es.json b/src/messages/es.json index d2fb5282a..6024e23e5 100644 --- a/src/messages/es.json +++ b/src/messages/es.json @@ -45,11 +45,14 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerOrcaRouter": "OrcaRouter", "providerOllama": "Ollama (Local)", "localOllama": "Modelo Ollama Local", "experimental": "Experimental", "useOpenRouter": "Usar API de OpenRouter", + "useOrcaRouter": "Use OrcaRouter API", "openRouterModel": "Modelo OpenRouter", + "orcaRouterModel": "OrcaRouter Model", "useOpenai": "Usar API de Openai", "openaiModel": "Modelo Openai", "useCustomModel": "Usar modelo personalizado", diff --git a/src/messages/fr.json b/src/messages/fr.json index a4a528393..4cdf830e5 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -45,11 +45,14 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerOrcaRouter": "OrcaRouter", "providerOllama": "Ollama (Local)", "localOllama": "Modèle Ollama local", "experimental": "Expérimental", "useOpenRouter": "Utiliser l’API OpenRouter", + "useOrcaRouter": "Use OrcaRouter API", "openRouterModel": "Modèle OpenRouter", + "orcaRouterModel": "OrcaRouter Model", "useOpenai": "Utiliser l’API OpenAI", "openaiModel": "Modèle OpenAI", "useCustomModel": "Utiliser un modèle personnalisé", diff --git a/src/messages/ja.json b/src/messages/ja.json index 28ea9460a..e22ae1e87 100644 --- a/src/messages/ja.json +++ b/src/messages/ja.json @@ -45,11 +45,14 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerOrcaRouter": "OrcaRouter", "providerOllama": "Ollama(ローカル)", "localOllama": "ローカルOllamaモデル", "experimental": "実験的", "useOpenRouter": "OpenRouter APIを使用", + "useOrcaRouter": "Use OrcaRouter API", "openRouterModel": "OpenRouterモデル", + "orcaRouterModel": "OrcaRouter Model", "useOpenai": "Openai APIを使用", "openaiModel": "Openaiモデル", "useCustomModel": "カスタムモデルを使用", diff --git a/src/messages/kr.json b/src/messages/kr.json index 1b23f645e..319812cd8 100644 --- a/src/messages/kr.json +++ b/src/messages/kr.json @@ -45,11 +45,14 @@ "providerGoogle": "구글", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerOrcaRouter": "OrcaRouter", "providerOllama": "Ollama (로컬)", "localOllama": "로컬 Ollama 모델", "experimental": "실험적", "useOpenRouter": "OpenRouter API 사용", + "useOrcaRouter": "Use OrcaRouter API", "openRouterModel": "OpenRouter 모델", + "orcaRouterModel": "OrcaRouter Model", "useOpenai": "Openai API 사용", "openaiModel": "Openai 모델", "useCustomModel": "사용자 정의 모델 사용", diff --git a/src/messages/pt-br.json b/src/messages/pt-br.json index 0d9699456..63dec56ff 100644 --- a/src/messages/pt-br.json +++ b/src/messages/pt-br.json @@ -45,11 +45,14 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerOrcaRouter": "OrcaRouter", "providerOllama": "Ollama (Local)", "localOllama": "Modelo Ollama Local", "experimental": "Experimental", "useOpenRouter": "Usar API OpenRouter", + "useOrcaRouter": "Use OrcaRouter API", "openRouterModel": "Modelo OpenRouter", + "orcaRouterModel": "OrcaRouter Model", "useOpenai": "Usar API OpenAI", "openaiModel": "Modelo OpenAI", "useCustomModel": "Usar modelo personalizado", diff --git a/src/messages/ru.json b/src/messages/ru.json index 17823a8bd..1d5c0d3fa 100644 --- a/src/messages/ru.json +++ b/src/messages/ru.json @@ -45,11 +45,14 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerOrcaRouter": "OrcaRouter", "providerOllama": "Ollama (локально)", "localOllama": "Локальная модель Ollama", "experimental": "Экспериментально", "useOpenRouter": "Использовать API OpenRouter", + "useOrcaRouter": "Use OrcaRouter API", "openRouterModel": "Модель OpenRouter", + "orcaRouterModel": "OrcaRouter Model", "useOpenai": "Использовать API OpenAI", "openaiModel": "Модель OpenAI", "useCustomModel": "Использовать пользовательскую модель", diff --git a/src/messages/vi.json b/src/messages/vi.json index 4d43d955e..561d1efec 100644 --- a/src/messages/vi.json +++ b/src/messages/vi.json @@ -45,11 +45,14 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerOrcaRouter": "OrcaRouter", "providerOllama": "Ollama (Cục bộ)", "localOllama": "Mô hình Ollama cục bộ", "experimental": "Thử nghiệm", "useOpenRouter": "Sử dụng API OpenRouter", + "useOrcaRouter": "Use OrcaRouter API", "openRouterModel": "Mô hình OpenRouter", + "orcaRouterModel": "OrcaRouter Model", "useOpenai": "Sử dụng API Openai", "openaiModel": "Mô hình Openai", "useCustomModel": "Sử dụng mô hình tùy chỉnh", diff --git a/src/messages/zh-tw.json b/src/messages/zh-tw.json index 8c18f9af5..69cded624 100644 --- a/src/messages/zh-tw.json +++ b/src/messages/zh-tw.json @@ -45,11 +45,14 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerOrcaRouter": "OrcaRouter", "providerOllama": "Ollama(本機)", "localOllama": "本機 Ollama 模型", "experimental": "實驗性", "useOpenRouter": "使用 OpenRouter API", + "useOrcaRouter": "Use OrcaRouter API", "openRouterModel": "OpenRouter 模型", + "orcaRouterModel": "OrcaRouter Model", "useOpenai": "使用 OpenAI API", "openaiModel": "OpenAI 模型", "useCustomModel": "使用自訂模型", diff --git a/src/messages/zh.json b/src/messages/zh.json index b0b60bea3..830eb1370 100644 --- a/src/messages/zh.json +++ b/src/messages/zh.json @@ -45,11 +45,14 @@ "providerGoogle": "Google", "providerOpenAI": "OpenAI", "providerOpenRouter": "OpenRouter", + "providerOrcaRouter": "OrcaRouter", "providerOllama": "Ollama (本地)", "localOllama": "本地Ollama模型", "experimental": "实验性", "useOpenRouter": "使用OpenRouter API", + "useOrcaRouter": "Use OrcaRouter API", "openRouterModel": "OpenRouter模型", + "orcaRouterModel": "OrcaRouter Model", "useOpenai": "使用Openai API", "openaiModel": "Openai 模型", "useCustomModel": "使用自定义模型", diff --git a/tests/unit/test_chat.py b/tests/unit/test_chat.py index 323d24131..c562ceed4 100644 --- a/tests/unit/test_chat.py +++ b/tests/unit/test_chat.py @@ -11,6 +11,7 @@ OllamaChatStreamer, OpenAIChatStreamer, OpenRouterChatStreamer, + OrcaRouterChatStreamer, ) @@ -25,6 +26,7 @@ ("dashscope", DashScopeChatStreamer), ("google", GoogleGenerativeChatStreamer), ("litellm", LiteLLMChatStreamer), + ("orcarouter", OrcaRouterChatStreamer), ("anthropic", AnthropicChatStreamer), ], ) @@ -43,6 +45,7 @@ def test_every_provider_is_registered(provider, expected): ("dashscope", DashScopeChatStreamer), ("google", GoogleGenerativeChatStreamer), ("litellm", LiteLLMChatStreamer), + ("orcarouter", OrcaRouterChatStreamer), ("anthropic", AnthropicChatStreamer), ], )