Skip to content

feat(models): add OrcaRouter provider - #3103

Closed
zhenjunchen-png wants to merge 1 commit into
langgenius:mainfrom
zhenjunchen-png:add-orcarouter-provider
Closed

feat(models): add OrcaRouter provider#3103
zhenjunchen-png wants to merge 1 commit into
langgenius:mainfrom
zhenjunchen-png:add-orcarouter-provider

Conversation

@zhenjunchen-png

@zhenjunchen-png zhenjunchen-png commented May 15, 2026

Copy link
Copy Markdown

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:

  • 108 LLM models including a virtual router orcarouter/auto (configurable strategies: cheapest / balanced / quality / adaptive LinUCB / gated_adaptive)
  • 5 embedding models (OpenAI 3 + Google 2)
  • 7 TTS models (OpenAI tts-1 / tts-1-hd / gpt-4o-mini-tts variants)
  • Native reasoning controls per upstream style: OpenAI reasoning_effort (flat) for gpt-5 / o-series, Anthropic thinking block for Claude 4.x, DeepSeek auto-reasoning for r1 / reasoner
  • extra_body routing exposure: per-request fallback model list via orcarouter_fallback_models + orcarouter_route parameters
  • customizable-model mode enabled — users can add any model not in the predefined list without waiting for a plugin update
  • 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 params, etc.)

Change Type

  • Documentation / non-plugin change
  • Non-LLM plugin (tools, extensions, datasource, etc.)
  • LLM plugin

Screenshots / Videos

ui1-plugins-list ui2-provider-card ui3-provider-configured ui4-model-types ui5-llm-list-auto-first ui6-llm-list-tail ui7-embedding-list ui8-tts-list A-auto B-gpt5-reasoning C-opus-thinking D1-fallback-dify D2-fallback-console E-error F1-system-embedding-model F2-kb-create-config F3-kb-available G-tts dify-version

log-llm.txt
log-embed-tts.txt

Before After
Provider absent from Dify Provider card visible; 120 models across LLM / Embedding / TTS; API key validated

LLM Plugin Checklist

Areas affected by this change (check all that apply)
  • Message flow (system messages, user ↔ assistant turn-taking)
  • Tool interaction flow (multi-round usage, Agent App and Agent Node)
  • Multimodal input (images, PDFs, audio, video, etc.) — vision-capable models declare vision / document features
  • Multimodal output (images, audio, video, etc.)
  • Structured output (JSON, XML, etc.) — response_format parameter exposed per upstream support
  • Token consumption metrics — usage reported back via OpenAI-compatible response
  • Other LLM functionality — reasoning controls (flat reasoning_effort / Anthropic thinking block), fallback routing via extra_body
  • New models / model parameter fixes

Version

  • Bumped top-level version in manifest.yaml (set to 0.0.1 for new plugin)
  • dify_plugin>=0.5.0,<0.6.0 declared in pyproject.toml and locked in uv.lock

Testing

  • Local deployment — Dify version: 1.14.1
  • SaaS (cloud.dify.ai)

Offline tests (pytest): 505 passing across YAML schema, endpoint URL normalization, _set_orca_extra_body wrapping, _set_reasoning_params translation 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 verified

Test scripts are shipped under tests/ so reviewers can re-verify with their own API key (ORCAROUTER_API_KEY env 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.

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>
@zhenjunchen-png
zhenjunchen-png deployed to models/orcarouter May 15, 2026 10:26 — with GitHub Actions Active
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. enhancement New feature or request labels May 15, 2026

@gemini-code-assist gemini-code-assist 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.

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)

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.

medium

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.")

Comment on lines +312 to +315
chunk_json: dict = TypeAdapter(dict[str, Any]).validate_json(
decoded_chunk
)
except ValidationError:

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.

medium

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.

Suggested change
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"))

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.

medium

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.

Suggested change
raise ValueError(chunk_json.get("error"))
raise ValueError(f"API Error: {chunk_json.get('error')}")

Comment on lines +378 to +383
if isinstance(delta_content, str):
full_assistant_content += delta_content
else:
full_assistant_content += "".join(
[c.data for c in delta_content]
)

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.

medium

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

@zhenjunchen-png

Copy link
Copy Markdown
Author

Per maintainer guidance, the OrcaRouter plugin submission has been migrated to the community plugins repository at langgenius/dify-plugins.

New PR: langgenius/dify-plugins#2413
Plugin source repository: https://github.com/zhenjunchen-png/dify-plugin-orcarouter

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant