diff --git a/frontend/public/icons/adapter-icons/OrcaRouter.png b/frontend/public/icons/adapter-icons/OrcaRouter.png new file mode 100644 index 0000000000..11ce68e1eb Binary files /dev/null and b/frontend/public/icons/adapter-icons/OrcaRouter.png differ diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py index f396bd618c..70c302bce2 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py +++ b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py @@ -534,6 +534,7 @@ def _validate_branded_openai_compatible( _NVIDIA_BUILD_API_BASE = "https://integrate.api.nvidia.com/v1" _OPENROUTER_API_BASE = "https://openrouter.ai/api/v1" +_ORCAROUTER_API_BASE = "https://api.orcarouter.ai/v1" _MINIMAX_API_BASE = "https://api.minimax.io/v1" _OPENROUTER_PROVIDER_PREFIX = "openrouter/" _MINIMAX_PROVIDER_PREFIX = "minimax/" @@ -578,6 +579,17 @@ def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: ) +class OrcaRouterLLMParameters(OpenAICompatibleLLMParameters): + """OpenAI-compatible adapter for OrcaRouter's model routing gateway.""" + + # Required str so a directly-constructed instance stays valid. + api_base: str = _ORCAROUTER_API_BASE + + @staticmethod + def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: + return _validate_branded_openai_compatible(adapter_metadata, _ORCAROUTER_API_BASE) + + class MiniMaxLLMParameters(BaseChatCompletionParameters): """Adapter for MiniMax's OpenAI- and Anthropic-compatible APIs.""" diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/__init__.py b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/__init__.py index a3a03c7da3..d3f58ff4a8 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/__init__.py +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/__init__.py @@ -12,6 +12,7 @@ from unstract.sdk1.adapters.llm1.openai import OpenAILLMAdapter from unstract.sdk1.adapters.llm1.openai_compatible import OpenAICompatibleLLMAdapter from unstract.sdk1.adapters.llm1.openrouter import OpenRouterLLMAdapter +from unstract.sdk1.adapters.llm1.orcarouter import OrcaRouterLLMAdapter from unstract.sdk1.adapters.llm1.vertexai import VertexAILLMAdapter adapters: dict[str, dict[str, Any]] = {} @@ -30,5 +31,6 @@ "OpenAILLMAdapter", "OpenAICompatibleLLMAdapter", "OpenRouterLLMAdapter", + "OrcaRouterLLMAdapter", "VertexAILLMAdapter", ] diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/orcarouter.py b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/orcarouter.py new file mode 100644 index 0000000000..1df0bfdc68 --- /dev/null +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/orcarouter.py @@ -0,0 +1,46 @@ +from typing import Any + +from unstract.sdk1.adapters.base1 import BaseAdapter, OrcaRouterLLMParameters +from unstract.sdk1.adapters.enums import AdapterTypes + +DESCRIPTION = ( + "Adapter for OrcaRouter's OpenAI-compatible model routing gateway " + "(orcarouter.ai). Supply a model name and your OrcaRouter API key; the " + "endpoint is preconfigured." +) + + +class OrcaRouterLLMAdapter(OrcaRouterLLMParameters, BaseAdapter): + @staticmethod + def get_id() -> str: + return "orcarouter|cc86ba36-f289-46a9-b60e-b2e8940c4385" + + @staticmethod + def get_metadata() -> dict[str, Any]: + return { + "name": "OrcaRouter", + "version": "1.0.0", + "adapter": OrcaRouterLLMAdapter, + "description": DESCRIPTION, + "is_active": True, + } + + @staticmethod + def get_name() -> str: + return "OrcaRouter" + + @staticmethod + def get_description() -> str: + return DESCRIPTION + + @staticmethod + def get_provider() -> str: + return "orcarouter" + + @staticmethod + def get_icon() -> str: + return "/icons/adapter-icons/OrcaRouter.png" + + @staticmethod + def get_adapter_type() -> AdapterTypes: + return AdapterTypes.LLM diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/orcarouter.json b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/orcarouter.json new file mode 100644 index 0000000000..4c94a1e9f4 --- /dev/null +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/orcarouter.json @@ -0,0 +1,112 @@ +{ + "title": "OrcaRouter", + "type": "object", + "required": [ + "adapter_name", + "api_key", + "model" + ], + "properties": { + "adapter_name": { + "type": "string", + "title": "Name", + "default": "", + "description": "Provide a unique name for this adapter instance. Example: orcarouter-1" + }, + "api_key": { + "type": "string", + "title": "API Key", + "format": "password", + "description": "Your OrcaRouter API key from [orcarouter.ai](https://www.orcarouter.ai). Keys begin with `sk-orca-`." + }, + "model": { + "type": "string", + "title": "Model", + "description": "The model slug as listed on [orcarouter.ai/models](https://www.orcarouter.ai/models). Examples: openai/gpt-5.5, anthropic/claude-opus-4.8, deepseek/deepseek-v4-flash" + }, + "api_base": { + "type": "string", + "format": "url", + "title": "API Base", + "default": "https://api.orcarouter.ai/v1", + "description": "OrcaRouter endpoint. Pre-filled with the default; change only if OrcaRouter moves the base URL." + }, + "max_tokens": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Maximum Output Tokens", + "default": 4096, + "description": "Maximum number of output tokens to limit LLM replies. Leave it empty to use the provider default. Sent as `max_completion_tokens` when Enable Reasoning is on." + }, + "max_retries": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Max Retries", + "default": 5, + "description": "The maximum number of times to retry a request if it fails." + }, + "timeout": { + "type": "number", + "minimum": 0, + "multipleOf": 1, + "title": "Timeout", + "default": 900, + "description": "Timeout in seconds." + }, + "enable_reasoning": { + "type": "boolean", + "title": "Enable Reasoning", + "default": false, + "description": "Toggle on for reasoning models to drop `temperature` and send `max_completion_tokens` and `reasoning_effort` via `extra_body` so the upstream API accepts them." + } + }, + "allOf": [ + { + "if": { + "properties": { + "enable_reasoning": { + "const": true + } + }, + "required": [ + "enable_reasoning" + ] + }, + "then": { + "properties": { + "reasoning_effort": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "default": "medium", + "title": "Reasoning Effort", + "description": "Sets the reasoning strength when Enable Reasoning is on." + } + }, + "required": [ + "reasoning_effort" + ] + } + }, + { + "if": { + "properties": { + "enable_reasoning": { + "const": false + } + }, + "required": [ + "enable_reasoning" + ] + }, + "then": { + "properties": {} + } + } + ] +} diff --git a/unstract/sdk1/tests/test_branded_openai_adapters.py b/unstract/sdk1/tests/test_branded_openai_adapters.py index c1556a3dd7..d40c91a492 100644 --- a/unstract/sdk1/tests/test_branded_openai_adapters.py +++ b/unstract/sdk1/tests/test_branded_openai_adapters.py @@ -7,6 +7,7 @@ NvidiaBuildLLMParameters, OpenAICompatibleEmbeddingParameters, OpenRouterLLMParameters, + OrcaRouterLLMParameters, ) from unstract.sdk1.adapters.constants import Common from unstract.sdk1.adapters.embedding1 import adapters as embedding_adapters @@ -18,9 +19,11 @@ from unstract.sdk1.adapters.llm1.minimax import MiniMaxLLMAdapter from unstract.sdk1.adapters.llm1.nvidia_build import NvidiaBuildLLMAdapter from unstract.sdk1.adapters.llm1.openrouter import OpenRouterLLMAdapter +from unstract.sdk1.adapters.llm1.orcarouter import OrcaRouterLLMAdapter _NVIDIA_BUILD_API_BASE = "https://integrate.api.nvidia.com/v1" _OPENROUTER_API_BASE = "https://openrouter.ai/api/v1" +_ORCAROUTER_API_BASE = "https://api.orcarouter.ai/v1" _MINIMAX_API_BASE = "https://api.minimax.io/v1" _MINIMAX_ANTHROPIC_API_BASE = "https://api.minimax.io/anthropic" _MINIMAX_CN_API_BASE = "https://api.minimaxi.com/v1" @@ -32,7 +35,12 @@ @pytest.mark.parametrize( "adapter", - [MiniMaxLLMAdapter, NvidiaBuildLLMAdapter, OpenRouterLLMAdapter], + [ + MiniMaxLLMAdapter, + NvidiaBuildLLMAdapter, + OpenRouterLLMAdapter, + OrcaRouterLLMAdapter, + ], ) def test_branded_llm_adapter_is_registered(adapter: type) -> None: adapter_id = adapter.get_id() @@ -47,6 +55,15 @@ def test_nvidia_llm_prefixes_model_via_custom_openai() -> None: assert validated["api_base"] == _NVIDIA_BUILD_API_BASE +def test_orcarouter_llm_prefixes_model_via_custom_openai() -> None: + validated = OrcaRouterLLMParameters.validate( + {"model": "deepseek/deepseek-v4-flash", "api_key": "k"} + ) + + assert validated["model"] == "custom_openai/deepseek/deepseek-v4-flash" + assert validated["api_base"] == _ORCAROUTER_API_BASE + + @pytest.mark.parametrize("model", ["MiniMax-M3", "MiniMax-M2.7"]) @pytest.mark.parametrize( ("api_base", "provider"), @@ -268,6 +285,7 @@ def test_openrouter_reasoning_survives_revalidation() -> None: (MiniMaxLLMParameters, _MINIMAX_API_BASE), (NvidiaBuildLLMParameters, _NVIDIA_BUILD_API_BASE), (OpenRouterLLMParameters, _OPENROUTER_API_BASE), + (OrcaRouterLLMParameters, _ORCAROUTER_API_BASE), ], ) def test_branded_llm_blank_api_base_falls_back_to_default( @@ -280,7 +298,12 @@ def test_branded_llm_blank_api_base_falls_back_to_default( @pytest.mark.parametrize( "params", - [MiniMaxLLMParameters, NvidiaBuildLLMParameters, OpenRouterLLMParameters], + [ + MiniMaxLLMParameters, + NvidiaBuildLLMParameters, + OpenRouterLLMParameters, + OrcaRouterLLMParameters, + ], ) def test_branded_llm_honours_api_base_override(params: type) -> None: validated = params.validate( @@ -296,6 +319,7 @@ def test_branded_llm_honours_api_base_override(params: type) -> None: (MiniMaxLLMAdapter, _MINIMAX_API_BASE), (NvidiaBuildLLMAdapter, _NVIDIA_BUILD_API_BASE), (OpenRouterLLMAdapter, _OPENROUTER_API_BASE), + (OrcaRouterLLMAdapter, _ORCAROUTER_API_BASE), ], ) def test_branded_llm_schema_exposes_api_base_with_default(