From cced475f3109c644a76268ab62666e34a3b3566b Mon Sep 17 00:00:00 2001 From: XiaoHuo888 Date: Sat, 15 Aug 2026 23:01:37 +0800 Subject: [PATCH] feat: add OrcaRouter as a named completion model Registers an orcarouter_auto model in morphik.toml (OpenAI-compatible endpoint https://api.orcarouter.ai/v1) and teaches LiteLLMCompletionModel to resolve an api_key_env config entry into the api_key passed to LiteLLM, so provider keys stay in the environment rather than in config. Co-Authored-By: Claude --- core/completion/litellm_completion.py | 16 +++++ .../test_litellm_completion_orcarouter.py | 59 +++++++++++++++++++ morphik.toml | 4 ++ 3 files changed, 79 insertions(+) create mode 100644 core/tests/unit/test_litellm_completion_orcarouter.py diff --git a/core/completion/litellm_completion.py b/core/completion/litellm_completion.py index ee75dbe0..32fedab5 100644 --- a/core/completion/litellm_completion.py +++ b/core/completion/litellm_completion.py @@ -1,4 +1,5 @@ import logging +import os import re # Import re for parsing model name from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union @@ -236,6 +237,21 @@ def __init__(self, model_key: str): self.model_config = settings.REGISTERED_MODELS[model_key] + # Resolve an api_key_env reference (e.g., "ORCAROUTER_API_KEY") into the + # api_key passed to LiteLLM, so keys stay in the environment rather than + # in morphik.toml. + api_key_env = self.model_config.get("api_key_env") + if api_key_env: + api_key = os.environ.get(api_key_env) + if api_key: + self.model_config["api_key"] = api_key + else: + logger.warning( + f"Model '{model_key}' references api_key_env '{api_key_env}', " + "but it is not set in the environment. Continuing without an api_key." + ) + self.model_config.pop("api_key_env", None) + # Check if it's an Ollama model for potential direct usage self.is_ollama = "ollama" in self.model_config.get("model_name", "").lower() self.ollama_api_base = None diff --git a/core/tests/unit/test_litellm_completion_orcarouter.py b/core/tests/unit/test_litellm_completion_orcarouter.py new file mode 100644 index 00000000..decab8a6 --- /dev/null +++ b/core/tests/unit/test_litellm_completion_orcarouter.py @@ -0,0 +1,59 @@ +"""Unit tests for LiteLLMCompletion api_key_env resolution (used by the OrcaRouter model).""" + +import os +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +from core.completion.litellm_completion import LiteLLMCompletionModel + +ORCAROUTER_MODEL = { + "model_name": "openai/orcarouter/auto", + "api_base": "https://api.orcarouter.ai/v1", + "api_key_env": "ORCAROUTER_API_KEY", +} + + +def _settings_with(registered_models): + return SimpleNamespace(REGISTERED_MODELS=registered_models) + + +class TestApiKeyEnvResolution: + def test_resolves_api_key_env_into_api_key(self): + os.environ["ORCAROUTER_API_KEY"] = "sk-orca-test" + try: + with patch( + "core.completion.litellm_completion.get_settings", + return_value=_settings_with({"orcarouter_auto": dict(ORCAROUTER_MODEL)}), + ): + model = LiteLLMCompletionModel("orcarouter_auto") + assert model.model_config["api_key"] == "sk-orca-test" + assert "api_key_env" not in model.model_config + finally: + os.environ.pop("ORCAROUTER_API_KEY", None) + + def test_missing_env_var_warns_and_omits_api_key(self): + os.environ.pop("ORCAROUTER_API_KEY", None) + with patch( + "core.completion.litellm_completion.get_settings", + return_value=_settings_with({"orcarouter_auto": dict(ORCAROUTER_MODEL)}), + ): + model = LiteLLMCompletionModel("orcarouter_auto") + assert "api_key" not in model.model_config + assert "api_key_env" not in model.model_config + + def test_model_without_api_key_env_is_unchanged(self): + with patch( + "core.completion.litellm_completion.get_settings", + return_value=_settings_with({"openai_gpt": {"model_name": "gpt-4.1"}}), + ): + model = LiteLLMCompletionModel("openai_gpt") + assert model.model_config == {"model_name": "gpt-4.1"} + + def test_unknown_model_key_raises(self): + with patch( + "core.completion.litellm_completion.get_settings", + return_value=_settings_with({}), + ), pytest.raises(ValueError, match="not found in registered_models"): + LiteLLMCompletionModel("does_not_exist") diff --git a/morphik.toml b/morphik.toml index b328b974..a1202243 100644 --- a/morphik.toml +++ b/morphik.toml @@ -32,6 +32,10 @@ gemini_flash = { model_name = "gemini/gemini-2.5-flash-preview-05-20" } # gemini # gemini_flash = { model_name = "gemini/gemini-2.5-pro-preview-06-05" } # { model_name = "claude-4-sonnet-20250514"} # { model_name = "gpt-4.1" } # { model_name = "gemini/gemini-2.5-pro-preview-06-05" } # {model_name = "o3-2025-04-16"} # { model_name = "claude-3-7-sonnet-latest"} # { model_name = "gemini/gemini-2.5-flash-preview-05-20" } # gemini-2.5-flash-preview-05-20 # gemini_flash = { model_name = "groq/meta-llama/llama-4-maverick-17b-128e-instruct"} +# OrcaRouter model (OpenAI-compatible gateway, https://www.orcarouter.ai) +# Set ORCAROUTER_API_KEY in the environment (keys start with sk-orca-). +orcarouter_auto = { model_name = "openai/orcarouter/auto", api_base = "https://api.orcarouter.ai/v1", api_key_env = "ORCAROUTER_API_KEY" } + # Ollama models (modify api_base based on your deployment) # - Local Ollama: "http://localhost:11434" (default) # - Morphik in Docker, Ollama local: "http://host.docker.internal:11434"