Repository navigation
Route OpenRouter requests by provider order - #3420
Conversation
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.
|
No actionable comments were generated in the recent review.
|
| 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:
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
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: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed-
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.
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.
|
@coderabbitai review |
✅ Action performed
|
Summary
An OpenRouter account whose privacy settings allow only zero-data-retention (ZDR) endpoints cannot use
openai/gpt-6.1-terrathrough 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:providergets 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."provider": {"order": ["azure/eu"]}gets 200, served by Azure.Dagu's OpenRouter provider had no way to send
provider. This PR addsprovider_order.YAML
provider_orderis a list of OpenRouter provider slugs. It goes in anyllmblock: the DAG-level one, thewithofchat.completion,with.llmofbrowser.run,computer.run, andxlsx.extract, an agent DAG's rootllm, anddagu browser session --llm.model:list entry can set its ownprovider_order, which replaces the shared one for that entry.base_url,api_key_name). camelCase keys such asbaseURLare legacy, and the decoder refuses them with a rename hint. So the key isprovider_order, notproviderOrder. In the IR JSON it isproviderOrder, next tobaseURLandapiKeyName.Behavior
llm.ConfiggainsProviderOrder, andruntime.NewLLMProviderpasses it in. The OpenRouter provider adds"provider": {"order": [...]}to every chat request it builds, streaming or not. It leavesproviderout when no order is set.runtime.EffectiveLLMConfigandruntime.NewLLMProvider. That covers chat, the agent planner, the browser model bridge,computer.run(including the generic computer-use session'sllm.ChatRequest), andxlsx.extract. So none of their request builders changed.allow_fallbackskeeps 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.provider_orderon a model whose provider is notopenrouter:field 'llm.provider_order': provider_order is only supported by the openrouter provider, not "anthropic". For a model list entry the path isllm.model[1].provider_order. A shared order that would reach a non-openrouter entry is refused atllm.provider_order. A mixed fallback list sets the order on its openrouter entries instead.${env.LLM_PROVIDER}) passes validation.NewLLMProviderrefuses it with the same message once it resolves to another provider.schemas/dag.schema.jsonlinks tointernal/cmn/schema/dag.schema.json) acceptprovider_orderas a list of strings on thellmblock and on model entries.Not changed
decision.evaluatecalls OpenRouter's separate decisions API with its ownwithconfig. It does not usellmsettings.dagu browser sessiongets no--provider-orderflag.--llm '{provider: openrouter, model: ..., provider_order: [azure/eu]}'already works.llmfield tables (step-types/llm/index.md,providers.md,reliability.md,yaml-specification.md) should gainprovider_orderwhen this ships in a release.Test plan
TestChatProviderOrder(openrouter, httptest): a configured order reaches the server asprovider.order. Without one the body has noproviderkey.TestProviderOrder(spec):computer.run, and for a gemini entry;TestDAGSchemaLLMProviderOrder: the schema accepts a list and refuses a scalar, both on the block and on a model entry.TestEffectiveLLMConfigProviderOrderandTestNewLLMProviderRefusesProviderOrder(runtime).TestChatProviderOrder(intg, end to end): achat.completionstep against a fake OpenRouter that answers 404 unlessazure/euis 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.provider;NewLLMProviderorEffectiveLLMConfig, or merging it the wrong way;daguwithprovider: openrouter,model: openai/gpt-6.1-terra,provider_order: [azure/eu],max_tokens: 16got 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.go fix -embedlit=false, andgofmt.llms.txtregenerated fromskills/dagu/references/steptypes.md.Summary by cubic
Adds
provider_orderto 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
llmblock acceptsprovider_order; amodel:list entry can set its own, which replaces the shared order for that entry.provider.orderon every request it builds, streaming or not.provider_orderfor any provider other thanopenrouter, and a provider given as a value reference is refused once it resolves to another provider.llms.txt, and the step-types reference accept the new key.Written for commit 89599a9. Summary will update on new commits.
Summary by CodeRabbit