Skip to content

Route OpenRouter requests by provider order - #3420

Merged
yohamta0 merged 2 commits into
mainfrom
openrouter-provider-order
Oct 29, 2026
Merged

yohamta0 merged 2 commits into
mainfrom
openrouter-provider-order

Conversation

@yohamta0

@yohamta0 yohamta0 commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Summary

An OpenRouter account whose privacy settings allow only zero-data-retention (ZDR) endpoints cannot use openai/gpt-6.1-terra through Dagu. OpenRouter's only ZDR endpoint for that model is Azure EU (azure/eu). It carries a regional surcharge, and OpenRouter passes over it unless the request names it:

  • A request without provider gets 404 "0 endpoints ... matching your guardrail restrictions and data policy ... ZDR violation (account settings)". The routing funnel shows "Filter by Regional Surcharge" dropped the endpoint.
  • The same request with "provider": {"order": ["azure/eu"]} gets 200, served by Azure.

Dagu's OpenRouter provider had no way to send provider. This PR adds provider_order.

YAML

steps:
  - action: chat.completion
    with:
      provider: openrouter
      model: openai/gpt-6.1-terra
      provider_order: [azure/eu]
      prompt: Hello.
  • provider_order is a list of OpenRouter provider slugs. It goes in any llm block: the DAG-level one, the with of chat.completion, with.llm of browser.run, computer.run, and xlsx.extract, an agent DAG's root llm, and dagu browser session --llm.
  • A model: list entry can set its own provider_order, which replaces the shared one for that entry.
  • Name: DAG keys are snake_case (base_url, api_key_name). camelCase keys such as baseURL are legacy, and the decoder refuses them with a rename hint. So the key is provider_order, not providerOrder. In the IR JSON it is providerOrder, next to baseURL and apiKeyName.

Behavior

  • Provider level. llm.Config gains ProviderOrder, and runtime.NewLLMProvider passes it in. The OpenRouter provider adds "provider": {"order": [...]} to every chat request it builds, streaming or not. It leaves provider out when no order is set.
    • Every step type builds its provider through runtime.EffectiveLLMConfig and runtime.NewLLMProvider. That covers chat, the agent planner, the browser model bridge, computer.run (including the generic computer-use session's llm.ChatRequest), and xlsx.extract. So none of their request builders changed.
    • Each model fallback gets its own provider, so an order set on one entry applies only to that entry.
  • allow_fallbacks keeps OpenRouter's default (true). The account's data policy still filters every fallback endpoint, so a fallback cannot break a ZDR setting. Refusing fallbacks would fail the request whenever the named endpoints are down. There is a comment at the call site.
  • Other providers are refused, not ignored. Silently dropping a routing order hides a misconfiguration: the step would reach endpoints the author tried to exclude.
    • Validation refuses provider_order on a model whose provider is not openrouter: field 'llm.provider_order': provider_order is only supported by the openrouter provider, not "anthropic". For a model list entry the path is llm.model[1].provider_order. A shared order that would reach a non-openrouter entry is refused at llm.provider_order. A mixed fallback list sets the order on its openrouter entries instead.
    • A provider given as a value reference (${env.LLM_PROVIDER}) passes validation. NewLLMProvider refuses it with the same message once it resolves to another provider.
    • A missing provider is still reported as "provider is required".
  • Schemas (schemas/dag.schema.json links to internal/cmn/schema/dag.schema.json) accept provider_order as a list of strings on the llm block and on model entries.

Not changed

  • decision.evaluate calls OpenRouter's separate decisions API with its own with config. It does not use llm settings.
  • dagu browser session gets no --provider-order flag. --llm '{provider: openrouter, model: ..., provider_order: [azure/eu]}' already works.
  • The docs site (dagucloud/docs) is not touched here. Its llm field tables (step-types/llm/index.md, providers.md, reliability.md, yaml-specification.md) should gain provider_order when this ships in a release.

Test plan

  • TestChatProviderOrder (openrouter, httptest): a configured order reaches the server as provider.order. Without one the body has no provider key.
  • TestProviderOrder (spec):
    • accepted for openrouter at the DAG level, inherited by a step, on a step, on a model entry, and with a value-reference provider;
    • refused for openai at the DAG level, for anthropic on a step, for a shared order reaching an anthropic entry of computer.run, and for a gemini entry;
    • a missing provider still reports "provider is required";
    • a non-list entry order is refused.
  • TestDAGSchemaLLMProviderOrder: the schema accepts a list and refuses a scalar, both on the block and on a model entry.
  • TestEffectiveLLMConfigProviderOrder and TestNewLLMProviderRefusesProviderOrder (runtime).
  • TestChatProviderOrder (intg, end to end): a chat.completion step against a fake OpenRouter that answers 404 unless azure/eu is named. The shared-order step succeeds, and a two-entry fallback list falls back from the entry without an order to the one with it.
  • Mutation checks. Each of these fails one of the tests above:
    • never or always sending provider;
    • dropping the order in NewLLMProvider or EffectiveLLMConfig, or merging it the wrong way;
    • removing the runtime refusal;
    • removing validation or the copy at the step or DAG level, or the copy of an entry's order;
    • ignoring an entry's or the shared order;
    • not deferring value references, refusing a missing provider, or allowing the wrong provider;
    • not parsing an entry's order, or not checking its type;
    • dropping either schema property.
  • A real request through a built dagu with provider: openrouter, model: openai/gpt-6.1-terra, provider_order: [azure/eu], max_tokens: 16 got 200 and answered "OK". The key used locally is not ZDR-restricted, so the same request without the order also succeeded. The run confirms OpenRouter accepts the object; the 404 above came from the restricted account.
  • golangci-lint v2.13.2 for linux, windows, and darwin on the changed packages, go fix -embedlit=false, and gofmt.
  • llms.txt regenerated from skills/dagu/references/steptypes.md.

Summary by cubic

Adds provider_order to LLM config so OpenRouter requests can name the upstream providers to try first, in order. This fixes 404 errors for OpenRouter accounts restricted to zero-data-retention endpoints, where a model's only eligible endpoint carries a regional surcharge and OpenRouter passes over it unless the request names it.

Behavior

  • Every llm block accepts provider_order; a model: list entry can set its own, which replaces the shared order for that entry.
  • The OpenRouter provider sends it as provider.order on every request it builds, streaming or not.
  • Validation refuses provider_order for any provider other than openrouter, and a provider given as a value reference is refused once it resolves to another provider.
  • The DAG schema, llms.txt, and the step-types reference accept the new key.

Written for commit 89599a9. Summary will update on new commits.

View guided diff Turn on auto-fix

Summary by CodeRabbit

  • New Features
    • OpenRouter configurations can specify an ordered list of upstream providers to try first. Set a shared order or override it for individual models.
  • Documentation
    • Updated the LLM configuration reference to describe provider ordering and where it can be configured, including DAG, step, browser, and computer settings.
  • Validation
    • Provider ordering is rejected for non-OpenRouter providers and for values that are not lists of strings.

An OpenRouter account that allows only zero-data-retention endpoints
cannot reach a model whose one such endpoint carries a regional
surcharge, since OpenRouter passes over that endpoint unless the
request names it. Dagu had no way to send OpenRouter's provider
routing object.

An llm block and each model entry now take provider_order, the
upstream providers OpenRouter tries first. The OpenRouter provider
sends it as provider.order on every request, so chat, agent, browser,
computer, and xlsx steps and model fallbacks all carry it. A model
entry's own order replaces the shared one. allow_fallbacks keeps
OpenRouter's default, as the account's data policy still filters
every fallback endpoint.

Only OpenRouter routes among upstream providers, so validation
refuses provider_order for any other provider, and a provider named
by a value reference is refused once it resolves.
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: bcb6d313-7eaa-43fb-9292-2123ea78cb55
Commits

Reviewing files that changed from the base of the PR and between 3fa68cb and 89599a9.

Files selected for processing: 1
  • internal/intg/chat_test.go

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Walkthrough

LLM configuration now accepts a shared provider_order and per-model overrides. Validation restricts nonempty orders to OpenRouter. Runtime selects the applicable order, and OpenRouter includes it in chat requests.

Changes: OpenRouter Provider Order

Layer / File(s) Summary
Configuration and validation
internal/cmn/schema/dag.schema.json, internal/cmn/schema/dag_schema_test.go, internal/spec/types/model.go, internal/spec/step.go, internal/spec/dag.go, internal/ir/llm.go, internal/spec/llm_test.go, llms.txt, skills/dagu/references/steptypes.md
Adds provider_order to the schema and configuration parsing. Validates provider compatibility, carries shared and per-model values into the IR, and documents the setting.
Runtime provider-order selection
internal/llm/types.go, internal/runtime/llm_provider.go, internal/runtime/llm_provider_test.go
Selects a nonempty model-level order over the shared order. Rejects nonempty orders for providers other than OpenRouter.
OpenRouter request routing
internal/llm/providers/openrouter/openrouter.go, internal/llm/providers/openrouter/openrouter_test.go, internal/intg/chat_test.go
Adds the configured order to OpenRouter requests under provider.order. Tests cover configured and unset orders, plus chat completion with shared and model-level settings.

Priority: Normal

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

Sequence Diagram(s)

sequenceDiagram
  participant DAGConfig
  participant Spec
  participant EffectiveLLMConfig
  participant NewLLMProvider
  participant OpenRouter
  participant OpenRouterAPI
  DAGConfig->>Spec: Parse and validate provider_order
  Spec->>EffectiveLLMConfig: Pass shared and model-level orders
  EffectiveLLMConfig->>NewLLMProvider: Select applicable order
  NewLLMProvider->>OpenRouter: Pass provider configuration
  OpenRouter->>OpenRouterAPI: Send request with provider.order
Loading
Merge Risk: Minimal · up to 89599

No actionable merge-blocking risk is established; configured provider ordering is propagated correctly through the supported paths.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: routing OpenRouter requests by provider order.
Description check Passed The description is detailed and covers the change summary, behavior, validation, documentation, testing, and scope. It does not use every template heading, but it provides the required information and…
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.
Full details: Description check

Explanation

The description is detailed and covers the change summary, behavior, validation, documentation, testing, and scope. It does not use every template heading, but it provides the required information and includes a comprehensive test plan.

Full details: Docstring Coverage

Explanation

Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 13 functions across 12 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

The model blocks of the end-to-end test hold no trailing newline, so
trimming them before formatting changed nothing.
@yohamta0

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yohamta0
yohamta0 merged commit 2e7245a into main Oct 29, 2026
19 checks passed
@yohamta0
yohamta0 deleted the openrouter-provider-order branch October 29, 2026 14:14
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.

1 participant