feat(models): add OrcaRouter provider - #3103
Conversation
OrcaRouter is an OpenAI-compatible LLM gateway routing across 40+ upstream providers with adaptive routing, fallback chains, and pay-below-list-price. This plugin exposes: - 108 LLM models including orcarouter/auto virtual router - 5 text-embedding models - 7 text-to-speech models Features: - Native reasoning controls per upstream style (OpenAI reasoning_effort flat, Anthropic thinking block, DeepSeek auto) - orcarouter_fallback_models + orcarouter_route exposed via extra_body - customizable-model mode enabled for user-defined model names - Per-model upstream quirks handled (gpt-5 no temperature, kimi-k2.6 fixed top_p=0.95, claude-opus-4.7 no top_k, grok-4.3 no penalty, etc.) Tested offline (505 pytest pass) + live (108 LLM, 5 embedding, 7 TTS pass). Verified locally on Dify 1.14.1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the OrcaRouter model provider plugin, an OpenAI-compatible gateway supporting over 40 upstream providers with features like adaptive routing and fallback chains. The implementation covers LLM, text embedding, and TTS models, including specialized parameter translation for reasoning models. Review feedback identifies several improvement opportunities: adding robust error handling for JSON schema parsing, optimizing stream response processing by replacing inefficient TypeAdapter usage with standard JSON loading, enhancing the clarity of API error messages, and simplifying assistant content accumulation logic by removing unreachable code.
| if response_format and response_format == "json_schema": | ||
| json_schema_str = model_parameters.get("json_schema") | ||
| if json_schema_str: | ||
| json_schema = json.loads(json_schema_str) |
There was a problem hiding this comment.
The json.loads call lacks error handling. If the json_schema parameter contains invalid JSON, it will raise a json.JSONDecodeError, causing the request to fail abruptly. It's safer to wrap this in a try-except block to provide a more helpful error message.
try:
json_schema = json.loads(json_schema_str)
except (json.JSONDecodeError, TypeError):
raise ValueError("Invalid JSON schema provided in parameters.")| chunk_json: dict = TypeAdapter(dict[str, Any]).validate_json( | ||
| decoded_chunk | ||
| ) | ||
| except ValidationError: |
There was a problem hiding this comment.
Instantiating TypeAdapter inside the loop for every stream chunk is inefficient due to the overhead involved. Using json.loads is a lighter and more standard approach in this context, as the chunk is expected to be a simple dictionary.
| chunk_json: dict = TypeAdapter(dict[str, Any]).validate_json( | |
| decoded_chunk | |
| ) | |
| except ValidationError: | |
| try: | |
| chunk_json: dict = json.loads(decoded_chunk) | |
| except (json.JSONDecodeError, TypeError): |
| break | ||
|
|
||
| if chunk_json.get("error") and chunk_json.get("choices") is None: | ||
| raise ValueError(chunk_json.get("error")) |
There was a problem hiding this comment.
Raising a ValueError with the raw error object from the API can lead to unhelpful error messages in the UI if the error is a complex dictionary. It's better to provide a more descriptive string representation.
| raise ValueError(chunk_json.get("error")) | |
| raise ValueError(f"API Error: {chunk_json.get('error')}") |
| if isinstance(delta_content, str): | ||
| full_assistant_content += delta_content | ||
| else: | ||
| full_assistant_content += "".join( | ||
| [c.data for c in delta_content] | ||
| ) |
There was a problem hiding this comment.
The else block is unreachable because delta_content is guaranteed to be a string by the _wrap_thinking_by_reasoning_content method. Additionally, the logic inside the else block is incorrect for string iteration (attempting to access a .data attribute on characters). This logic should be simplified.
full_assistant_content += delta_content|
Per maintainer guidance, the OrcaRouter plugin submission has been migrated to the community plugins repository at New PR: langgenius/dify-plugins#2413 Closing this in favor of the dedicated community submission flow. Thanks to @gemini-code-assist for the automated review feedback — the suggestions have been noted and will be addressed in subsequent iterations. |
Summary
Adds OrcaRouter as an official LLM provider plugin. OrcaRouter is an OpenAI-compatible gateway routing requests across 40+ upstream providers (OpenAI, Anthropic, Google, DeepSeek, Qwen, Grok, Kimi, MiniMax, z.ai, etc.) with adaptive routing, fallback chains, and below-list-price billing.
Tracking: #3102
Highlights:
orcarouter/auto(configurable strategies:cheapest/balanced/quality/adaptiveLinUCB /gated_adaptive)tts-1/tts-1-hd/gpt-4o-mini-ttsvariants)reasoning_effort(flat) for gpt-5 / o-series, Anthropicthinkingblock for Claude 4.x, DeepSeek auto-reasoning for r1 / reasonerextra_bodyrouting exposure: per-request fallback model list viaorcarouter_fallback_models+orcarouter_routeparameterscustomizable-modelmode enabled — users can add any model not in the predefined list without waiting for a plugin updateChange Type
Screenshots / Videos
log-llm.txt
log-embed-tts.txt
LLM Plugin Checklist
Areas affected by this change (check all that apply)
vision/documentfeaturesresponse_formatparameter exposed per upstream supportreasoning_effort/ Anthropicthinkingblock), fallback routing viaextra_bodyVersion
versioninmanifest.yaml(set to0.0.1for new plugin)dify_plugin>=0.5.0,<0.6.0declared inpyproject.tomland locked inuv.lockTesting
Offline tests (
pytest): 505 passing across YAML schema, endpoint URL normalization,_set_orca_extra_bodywrapping,_set_reasoning_paramstranslation across all provider styles (OpenAI / Anthropic / others).Live integration tests (against
https://api.orcarouter.ai/v1):tests/validate_classification.py— 108/108 LLM models verified (streaming, parameter conformance, fallback routing)tests/validate_embedding_tts.py— 5/5 embedding + 7/7 TTS verifiedTest scripts are shipped under
tests/so reviewers can re-verify with their own API key (ORCAROUTER_API_KEYenv var). The pytest-based live tests skip cleanly when the key is not set, so CI passes on first push without a configured secret.Disclosure
I'm an engineer on the OrcaRouter team.