Skip to content

[NOT-780] feat(llm): add OrcaRouter as a named LLM provider - #893

Open
XiaoHuo888-hue wants to merge 1 commit into
nottelabs:mainfrom
XiaoHuo888-hue:add-orcarouter-provider
Open

[NOT-780] feat(llm): add OrcaRouter as a named LLM provider#893
XiaoHuo888-hue wants to merge 1 commit into
nottelabs:mainfrom
XiaoHuo888-hue:add-orcarouter-provider

Conversation

@XiaoHuo888-hue

@XiaoHuo888-hue XiaoHuo888-hue commented Aug 10, 2026

Copy link
Copy Markdown

Summary

  • Add OrcaRouter as a named LLM provider, mirroring the existing OpenRouter wiring so the provider registry, config toggle and API-key handling stay consistent.
  • notte-core:
    • New LlmProvider.orcarouter enum member plus an enable_orcarouter() toggle gated by the ENABLE_ORCAROUTER env var.
    • New LlmModel.orcarouter = "orcarouter/auto" auto-routing model (a fixed model is preferable when strict structured output is required).
    • New LlmModel.get_orcarouter_model() that maps Notte's internal provider prefixes to the namespaced ids OrcaRouter serves (e.g. gemini/gemini-2.5-flashgoogle/gemini-2.5-flash, moonshot/kimi-k2.5kimi/kimi-k2.5).
  • notte-llm:
    • LLMEngine._get_model() prefixes the mapped id with openai/ so litellm uses its OpenAI-compatible path and forwards the full namespaced id to https://api.orcarouter.ai/v1.
    • LLMEngine.completion() passes base_url and ORCAROUTER_API_KEY when OrcaRouter mode is enabled.
    • structured_completion() uses the OpenAI json_schema wrapper for OrcaRouter-routed models (works across OpenAI, Anthropic, Google and DeepSeek backends).
  • .env.example: added ORCAROUTER_API_KEY= and ENABLE_ORCAROUTER=false.
  • Tests: tests/config/test_orcarouter_provider.py, tests/llms/test_orcarouter_models.py, and an engine routing test in tests/llms/test_engine.py.

OrcaRouter

This registers OrcaRouter the same way the existing OpenRouter provider is wired, so the model registry, config toggle and docs stay consistent. OrcaRouter is an OpenAI-compatible gateway: one ORCAROUTER_API_KEY (keys start with sk-orca-) unlocks 150+ models from OpenAI, Anthropic, Google, DeepSeek, Qwen, MiniMax and xAI behind a single https://api.orcarouter.ai/v1 endpoint. It also runs gateway-level, zero-trust security for AI agents on the same endpoint - screening every prompt/response and governing every tool call on a default-deny basis, with no application code changes.

I'm an engineer on the OrcaRouter team.

Test plan

  • pytest tests/config/test_orcarouter_provider.py tests/llms/test_engine.py - all pass
  • ruff check on the touched files
  • Live L3 with a real key through the actual engine path: chat completions for openai/gpt-4o, google/gemini-2.5-flash, anthropic/claude-sonnet-4.6, orcarouter/auto all return 200, and structured output (OpenAI json_schema) succeeds on both OpenAI and Anthropic backends.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features

    • Added OrcaRouter as a configurable language model provider.
    • Added support for enabling or disabling OrcaRouter through environment settings.
    • Added automatic model routing and compatibility handling for supported models.
    • Added support for structured completions through OrcaRouter.
  • Tests

    • Added coverage for provider configuration, model mapping, completion requests, and agent interactions across supported models.

Mirror the existing OpenRouter wiring so OrcaRouter is selectable as a
provider behind the ENABLE_ORCAROUTER env toggle:
- LlmProvider.orcarouter + LlmModel.orcarouter + enable_orcarouter()
- LlmModel.get_orcarouter_model() maps Notte provider prefixes to the
  namespaced ids OrcaRouter serves
- LLMEngine routes through litellm's OpenAI-compatible path with base_url
  https://api.orcarouter.ai/v1 and ORCAROUTER_API_KEY
- structured_completion uses the OpenAI json_schema wrapper for all
  OrcaRouter-routed upstreams
- tests: config mapping, engine routing, agent model smoke list
- .env.example: ORCAROUTER_API_KEY + ENABLE_ORCAROUTER
@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown

PR author is not in the allowed authors list.

@superagent-security

Copy link
Copy Markdown

🚨 Contributor flagged. Click here for more info: Superagent Dashboard

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds OrcaRouter environment configuration, provider and model definitions, API-key resolution, and model normalization. Updates LLMEngine to detect OrcaRouter models, use OpenAI-compatible structured completions, rewrite model identifiers, and send requests to the OrcaRouter endpoint. Adds unit, completion, and agent integration tests for model conversion and supported OrcaRouter models.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • nottelabs/notte#700: Adds analogous routed-provider configuration, normalization, engine routing, and tests.
  • nottelabs/notte#734: Adds provider-specific model detection and structured-completion routing in engine.py.
  • nottelabs/notte#747: Extends provider and model enums with routed-provider normalization tests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding OrcaRouter as a named LLM provider.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superagent found 1 security concern(s).

temperature = LlmModel.get_temperature(model, temperature)
completion_kwargs: dict[str, Any] = {}
if enable_orcarouter():
completion_kwargs["base_url"] = ORCAROUTER_BASE_URL

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Missing ORCAROUTER_API_KEY causes OpenAI API key leakage to third-party endpoint

ENABLE_ORCAROUTER=true without ORCAROUTER_API_KEY leaks OPENAI_API_KEY to api.orcarouter.ai via LiteLLM fallback.

Require ORCAROUTER_API_KEY before setting base_url, or explicitly pass api_key to prevent LiteLLM provider fallback.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name="packages/notte-llm/src/notte_llm/engine.py">
<violation number="1" location="packages/notte-llm/src/notte_llm/engine.py:586">
<priority>P1</priority>
<title>Missing ORCAROUTER_API_KEY causes OpenAI API key leakage to third-party endpoint</title>
<evidence>When `ENABLE_ORCAROUTER=true` is set but `ORCAROUTER_API_KEY` is missing, the code unconditionally configures `base_url=https://api.orcarouter.ai/v1` while only conditionally adding `api_key` to `completion_kwargs`. Because `_get_model()` prefixes the model with `openai/` for LiteLLM compatibility, LiteLLM's OpenAI provider falls back to the `OPENAI_API_KEY` environment variable when no explicit `api_key` is passed. This causes the user's OpenAI API key to be transmitted to the OrcaRouter endpoint without explicit consent.</evidence>
<recommendation>Validate that `ORCAROUTER_API_KEY` is present before setting `base_url`. Raise a clear `ValueError` or configuration error when `ENABLE_ORCAROUTER=true` but the required API key is missing. For example:
```python
if enable_orcarouter():
    orcarouter_api_key = os.environ.get('ORCAROUTER_API_KEY')
    if not orcarouter_api_key:
        raise ValueError('ORCAROUTER_API_KEY must be set when ENABLE_ORCAROUTER=true')
    completion_kwargs['base_url'] = ORCAROUTER_BASE_URL
    completion_kwargs['api_key'] = orcarouter_api_key
```</recommendation>
</violation>
</file>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/config/test_orcarouter_provider.py (1)

94-110: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the supported model ID.

The namespace assertion accepts model IDs that OrcaRouter does not serve. Use ORCAROUTER_MODELS to assert that each converted result is a supported model ID.

Proposed test change
         result = LlmModel.get_orcarouter_model(model.value)
-        assert result.startswith(
-            (
-                "openai/",
-                "anthropic/",
-                "google/",
-                "deepseek/",
-                "minimax/",
-                "kimi/",
-                "grok/",
-                "z-ai/",
-                "orcarouter/",
-            )
-        )
+        assert result in ORCAROUTER_MODELS
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/config/test_orcarouter_provider.py` around lines 94 - 110, Update
test_all_models_can_be_converted_to_orcarouter to assert that each result from
LlmModel.get_orcarouter_model(model.value) is contained in ORCAROUTER_MODELS,
replacing the broad namespace-prefix assertion while preserving the
parameterization over every LlmModel value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/notte-core/src/notte_core/common/config.py`:
- Around line 163-166: The default LlmModel.orcarouter configuration must not be
advertised when only OpenRouter is enabled, because it is incorrectly routed as
openrouter/orcarouter/auto. Update the model availability or routing logic
around LlmModel.orcarouter and LLMEngine._get_model so it is included only when
ENABLE_ORCAROUTER is enabled, or explicitly route orcarouter/ models through
ORCAROUTER_BASE_URL regardless of the global toggle.

In `@tests/llms/test_engine.py`:
- Around line 63-67: Wrap the completion call and any related assertions in a
try/finally within the ENABLE_ORCAROUTER environment patch, and reset
notte_config._enable_orcarouter to None in the finally block. Remove the
existing trailing reset so cleanup occurs even when the test fails.

In `@tests/llms/test_orcarouter_models.py`:
- Around line 42-47: Update check_orcarouter_available to read
ORCAROUTER_API_KEY and return True only when its value contains non-whitespace
characters; treat missing, empty, and whitespace-only values as unavailable.

---

Nitpick comments:
In `@tests/config/test_orcarouter_provider.py`:
- Around line 94-110: Update test_all_models_can_be_converted_to_orcarouter to
assert that each result from LlmModel.get_orcarouter_model(model.value) is
contained in ORCAROUTER_MODELS, replacing the broad namespace-prefix assertion
while preserving the parameterization over every LlmModel value.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b1b0271-361a-4d9c-bdc9-6024295b6764

📥 Commits

Reviewing files that changed from the base of the PR and between caf8919 and f0fcbeb.

📒 Files selected for processing (6)
  • .env.example
  • packages/notte-core/src/notte_core/common/config.py
  • packages/notte-llm/src/notte_llm/engine.py
  • tests/config/test_orcarouter_provider.py
  • tests/llms/test_engine.py
  • tests/llms/test_orcarouter_models.py

Comment on lines +163 to +166
# Auto-routing OrcaRouter model. Prefer a fixed model (e.g. openai/gpt-4o)
# when using strict structured output, as orcarouter/auto does not
# guarantee json_schema compliance across all upstreams.
orcarouter = "orcarouter/auto"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not advertise orcarouter/auto for an OpenRouter-only configuration.

When ENABLE_OPENROUTER=true and ENABLE_ORCAROUTER=false, Line 103 makes LlmProvider.orcarouter.apikey_name return OPENROUTER_API_KEY. LlmModel.valid() then includes this model. LLMEngine._get_model() sends it as openrouter/orcarouter/auto instead of using ORCAROUTER_BASE_URL.

Exclude LlmModel.orcarouter unless OrcaRouter routing is enabled, or route explicit orcarouter/ models through OrcaRouter independent of the global toggle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/notte-core/src/notte_core/common/config.py` around lines 163 - 166,
The default LlmModel.orcarouter configuration must not be advertised when only
OpenRouter is enabled, because it is incorrectly routed as
openrouter/orcarouter/auto. Update the model availability or routing logic
around LlmModel.orcarouter and LLMEngine._get_model so it is included only when
ENABLE_ORCAROUTER is enabled, or explicitly route orcarouter/ models through
ORCAROUTER_BASE_URL regardless of the global toggle.

Comment thread tests/llms/test_engine.py
Comment on lines +63 to +67
with patch.dict(os.environ, {"ENABLE_ORCAROUTER": "true", "ORCAROUTER_API_KEY": "sk-orca-test"}):
notte_config._enable_orcarouter = None # Reset cached value
with patch("litellm.acompletion", return_value=mock_response) as mock_acompletion:
response = await llm_engine.completion(messages=messages, model=model)
notte_config._enable_orcarouter = None # Reset cached value

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset _enable_orcarouter in a finally block.

If llm_engine.completion() or a later assertion fails, Line 67 does not run. patch.dict restores the environment, but the cached value remains True. Later tests can then route through OrcaRouter unexpectedly.

Proposed fix
 with patch.dict(os.environ, {"ENABLE_ORCAROUTER": "true", "ORCAROUTER_API_KEY": "sk-orca-test"}):
     notte_config._enable_orcarouter = None  # Reset cached value
-    with patch("litellm.acompletion", return_value=mock_response) as mock_acompletion:
-        response = await llm_engine.completion(messages=messages, model=model)
-    notte_config._enable_orcarouter = None  # Reset cached value
+    try:
+        with patch("litellm.acompletion", return_value=mock_response) as mock_acompletion:
+            response = await llm_engine.completion(messages=messages, model=model)
+    finally:
+        notte_config._enable_orcarouter = None  # Reset cached value
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
with patch.dict(os.environ, {"ENABLE_ORCAROUTER": "true", "ORCAROUTER_API_KEY": "sk-orca-test"}):
notte_config._enable_orcarouter = None # Reset cached value
with patch("litellm.acompletion", return_value=mock_response) as mock_acompletion:
response = await llm_engine.completion(messages=messages, model=model)
notte_config._enable_orcarouter = None # Reset cached value
with patch.dict(os.environ, {"ENABLE_ORCAROUTER": "true", "ORCAROUTER_API_KEY": "sk-orca-test"}):
notte_config._enable_orcarouter = None # Reset cached value
try:
with patch("litellm.acompletion", return_value=mock_response) as mock_acompletion:
response = await llm_engine.completion(messages=messages, model=model)
finally:
notte_config._enable_orcarouter = None # Reset cached value
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/llms/test_engine.py` around lines 63 - 67, Wrap the completion call and
any related assertions in a try/finally within the ENABLE_ORCAROUTER environment
patch, and reset notte_config._enable_orcarouter to None in the finally block.
Remove the existing trailing reset so cleanup occurs even when the test fails.

Comment on lines +42 to +47
def check_orcarouter_available() -> bool:
"""Check if OrcaRouter API key is available.

Note: Relies on load_dotenv() having been called at module import time.
"""
return os.getenv("ORCAROUTER_API_KEY") is not None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject empty OrcaRouter API keys.

An empty ORCAROUTER_API_KEY is not None, so the live test suite runs without usable credentials. Return False for empty or whitespace-only values.

Proposed fix
 def check_orcarouter_available() -> bool:
     """Check if OrcaRouter API key is available.
 
     Note: Relies on load_dotenv() having been called at module import time.
     """
-    return os.getenv("ORCAROUTER_API_KEY") is not None
+    return bool(os.getenv("ORCAROUTER_API_KEY", "").strip())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def check_orcarouter_available() -> bool:
"""Check if OrcaRouter API key is available.
Note: Relies on load_dotenv() having been called at module import time.
"""
return os.getenv("ORCAROUTER_API_KEY") is not None
def check_orcarouter_available() -> bool:
"""Check if OrcaRouter API key is available.
Note: Relies on load_dotenv() having been called at module import time.
"""
return bool(os.getenv("ORCAROUTER_API_KEY", "").strip())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/llms/test_orcarouter_models.py` around lines 42 - 47, Update
check_orcarouter_available to read ORCAROUTER_API_KEY and return True only when
its value contains non-whitespace characters; treat missing, empty, and
whitespace-only values as unavailable.

@giordano-lucas

Copy link
Copy Markdown
Member

@giordano-lucas giordano-lucas changed the title feat(llm): add OrcaRouter as a named LLM provider [NOT-780] feat(llm): add OrcaRouter as a named LLM provider Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants