feat: add OrcaRouter as a named LLM provider - #302
Conversation
Adds `orcarouter` to the provider registry (auto-detect order, env key, model default/env), with dedicated text + structured-extraction adapters hitting https://api.orcarouter.ai/v1. - TEXT_ADAPTERS: callOrcaRouterText (OpenAI-compatible, vision via data: URL) — used by research/agent synthesis - ADAPTERS: callOrcaRouter (json_object + client-side schema validation + one retry) — used by extract's structured fallback - select/model-select/keystore/init/TUI wiring: ORCAROUTER_API_KEY, WIGOLO_LLM_MODEL_ORCAROUTER, --provider validation, config wizard, keystore storage (env-only in the picker, mirroring groq) - vision gate in browser-pool accepts orcarouter - tests + docs updated Co-Authored-By: Claude <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds OrcaRouter as an OpenAI-compatible LLM provider. The change covers provider selection, model and API-key resolution, CLI setup, keystore support, extraction, text and vision adapters, documentation, and tests. ChangesOrcaRouter provider support
Estimated code review effort: 3 (Moderate) | ~30 minutes Mergeability Score: 🟡 Moderate · up to The extraction fallback can ignore the configured model and reuse results across different model selections, so users may receive extraction from an unintended model. This concrete correctness issue should be fixed or explicitly accepted before merge. Sequence Diagram(s)sequenceDiagram
participant CLI
participant ProviderSelection
participant OrcaRouterAdapter
participant OrcaRouter
CLI->>ProviderSelection: select orcarouter
ProviderSelection->>OrcaRouterAdapter: resolve model and API key
OrcaRouterAdapter->>OrcaRouter: send OpenAI-compatible request
OrcaRouter-->>OrcaRouterAdapter: return response
OrcaRouterAdapter-->>CLI: return validated result and metadata
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 Warning |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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 `@docs/configuration.md`:
- Around line 102-103: Update the configuration provider table to document
WIGOLO_LLM_MODEL_ORCAROUTER, including that it overrides WIGOLO_LLM_MODEL when
the orcarouter provider is selected.
In `@src/extraction/llm-fallback.ts`:
- Line 47: Update extractWithLLM to resolve the configured extraction model
before the cache lookup, include the resolved model in the cache key to
distinguish provider configurations, and pass it to the selected adapter through
modelOverride. Preserve the existing provider dispatch while ensuring every
adapter call uses the resolved model.
In `@src/integrations/cloud/llm/text-adapters.ts`:
- Around line 168-180: Update the TextCallOpts documentation to include
orcarouter among the supported providers, keeping the public contract aligned
with callOrcaRouterText’s opts.image vision support.
In `@tests/unit/extraction/llm-fallback.test.ts`:
- Line 73: Update the beforeEach cleanup in the llm-fallback test fixture to
delete ORCAROUTER_API_KEY alongside the other provider keys, ensuring the
no-provider test cannot inherit the host environment value.
In `@tests/unit/extraction/llm/orcarouter.test.ts`:
- Around line 31-40: Update the test around callOrcaRouter to capture the
arguments passed to the OpenAI constructor rather than relying on the mocked
client instance. Assert that the constructor options include baseURL set to
https://api.orcarouter.ai/v1, while preserving the existing response_format
assertion.
Apply the same fix in
`@tests/unit/integrations/cloud/llm/text-adapters-vision.test.ts` around lines 117
- 135: Same missing baseURL assertion for the text vision adapter.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: dd48bd2d-b203-4bad-b70d-9bd4ac5f235b
📒 Files selected for processing (30)
README.mddocs/configuration.mdsrc/cli/init.tssrc/cli/tui/actions/verify-e2e.tssrc/cli/tui/extras-prompt.tssrc/cli/tui/flags.tssrc/cli/tui/schema/llm.tssrc/extraction/llm-fallback.tssrc/fetch/browser-pool.tssrc/integrations/cloud/llm/model-select.tssrc/integrations/cloud/llm/orcarouter.tssrc/integrations/cloud/llm/run.tssrc/integrations/cloud/llm/select.tssrc/integrations/cloud/llm/text-adapters.tssrc/integrations/cloud/llm/types.tssrc/security/key-store.tstests/integration/llm-fallback-e2e.test.tstests/integration/zero-touch-just-works.test.tstests/unit/agent/pipeline.llm-gate-keystore.test.tstests/unit/cli/tui/actions/provider-actions.test.tstests/unit/cli/tui/schema/llm-no-custom.test.tstests/unit/cli/tui/schema/llm.test.tstests/unit/extraction/llm-fallback.test.tstests/unit/extraction/llm/orcarouter.test.tstests/unit/extraction/llm/select.test.tstests/unit/extraction/llm/types.test.tstests/unit/integrations/cloud/llm/text-adapters-vision.test.tstests/unit/integrations/llm-runner.test.tstests/unit/research/pipeline.llm-gate-keystore.test.tstests/unit/research/synthesis-local.test.ts
| | `WIGOLO_LLM_PROVIDER` | unset | `anthropic`, `openai`, `gemini`, `groq`, `orcarouter`, or `ollama` (any local OpenAI-compatible server). | | ||
| | `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` / `GROQ_API_KEY` / `ORCAROUTER_API_KEY` | unset | Per-provider keys, read from env. | |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Document WIGOLO_LLM_MODEL_ORCAROUTER.
The provider table documents ORCAROUTER_API_KEY, but it does not document the provider-specific model override stated in this PR. Add WIGOLO_LLM_MODEL_ORCAROUTER and describe its precedence relative to WIGOLO_LLM_MODEL.
🤖 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 `@docs/configuration.md` around lines 102 - 103, Update the configuration
provider table to document WIGOLO_LLM_MODEL_ORCAROUTER, including that it
overrides WIGOLO_LLM_MODEL when the orcarouter provider is selected.
| openai: callOpenAI, | ||
| gemini: callGemini, | ||
| groq: callGroq, | ||
| orcarouter: callOrcaRouter, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline src/integrations/cloud/llm/model-select.ts --items all
rg -n -C 4 'WIGOLO_LLM_MODEL_ORCAROUTER|modelOverride|callOrcaRouter|extractWithLLM' src testsRepository: KnockOutEZ/wigolo
Length of output: 39669
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat -n src/integrations/cloud/llm/model-select.ts
sed -n '1,150p' src/extraction/llm-fallback.ts
sed -n '1,230p' tests/unit/extraction/llm-fallback.test.ts
sed -n '1,120p' src/integrations/cloud/llm/types.tsRepository: KnockOutEZ/wigolo
Length of output: 13622
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
fallback = Path("src/extraction/llm-fallback.ts").read_text()
select = Path("src/integrations/cloud/llm/model-select.ts").read_text()
adapters = {
name: Path(f"src/integrations/cloud/llm/{name}.ts").read_text()
for name in ("anthropic", "openai", "gemini", "groq", "orcarouter")
}
call = re.search(
r"result\s*=\s*await\s+ADAPTERS\[provider\]\((.*?)\n\s*\),\s*apiKey",
fallback,
re.S,
)
assert call, "adapter dispatch not found"
dispatch = call.group(1)
print("dispatch_has_modelOverride:", "modelOverride" in dispatch)
print("resolveModel_reads_orcarouter_env:",
"orcarouter: 'WIGOLO_LLM_MODEL_ORCAROUTER'" in select)
for name, source in adapters.items():
has_override = "opts.modelOverride" in source
print(f"{name}_uses_modelOverride:", has_override)
assert has_override, f"{name} does not consume modelOverride"
assert "WIGOLO_LLM_MODEL_ORCAROUTER" in select
assert "modelOverride" not in dispatch
PYRepository: KnockOutEZ/wigolo
Length of output: 272
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '85,112p' src/extraction/llm-fallback.ts
rg -n 'const model = opts\.modelOverride|WIGOLO_LLM_MODEL_ORCAROUTER|resolveModel|ADAPTERS\[provider\]' \
src/integrations/cloud/llm src/extraction/llm-fallback.ts
python3 - <<'PY'
from pathlib import Path
fallback = Path("src/extraction/llm-fallback.ts").read_text().splitlines()
start = next(i for i, line in enumerate(fallback) if "ADAPTERS[provider]" in line)
dispatch = "\n".join(fallback[start:start + 4])
print(dispatch)
print("dispatch_has_modelOverride:", "modelOverride" in dispatch)
assert "modelOverride" not in dispatch
select = Path("src/integrations/cloud/llm/model-select.ts").read_text()
assert "orcarouter: 'WIGOLO_LLM_MODEL_ORCAROUTER'" in select
print("resolveModel_reads_orcarouter_env: True")
for name in ("anthropic", "openai", "gemini", "groq", "orcarouter"):
source = Path(f"src/integrations/cloud/llm/{name}.ts").read_text()
assert "opts.modelOverride" in source
print(f"{name}_uses_modelOverride: True")
PYRepository: KnockOutEZ/wigolo
Length of output: 2272
Resolve and propagate the configured extraction model.
extractWithLLM calls adapters without modelOverride, so provider-specific model settings are ignored. Its ${provider}:default cache key also collides across configured models. Resolve the model before cache lookup, include it in the cache key, and pass it as modelOverride.
🤖 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 `@src/extraction/llm-fallback.ts` at line 47, Update extractWithLLM to resolve
the configured extraction model before the cache lookup, include the resolved
model in the cache key to distinguish provider configurations, and pass it to
the selected adapter through modelOverride. Preserve the existing provider
dispatch while ensuring every adapter call uses the resolved model.
| export async function callOrcaRouterText(opts: TextCallOpts, apiKey: string): Promise<TextCallResult> { | ||
| const { default: OpenAI } = await import('openai'); | ||
| const client = new OpenAI({ apiKey, baseURL: 'https://api.orcarouter.ai/v1' }); | ||
| const start = Date.now(); | ||
| const content = opts.image | ||
| ? [ | ||
| { type: 'text' as const, text: opts.prompt }, | ||
| { | ||
| type: 'image_url' as const, | ||
| image_url: { url: `data:${opts.image.mediaType};base64,${opts.image.data}` }, | ||
| }, | ||
| ] | ||
| : opts.prompt; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Document OrcaRouter vision support in TextCallOpts.
callOrcaRouterText now accepts opts.image, but the TextCallOpts documentation at Line 27 still lists only anthropic/openai/gemini. Add orcarouter so the public adapter contract matches the implementation.
🤖 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 `@src/integrations/cloud/llm/text-adapters.ts` around lines 168 - 180, Update
the TextCallOpts documentation to include orcarouter among the supported
providers, keeping the public contract aligned with callOrcaRouterText’s
opts.image vision support.
| expect(out.values).toEqual({}); | ||
| expect(out.warnings.join(' ')).toMatch(/ANTHROPIC_API_KEY/); | ||
| expect(out.warnings.join(' ')).toMatch(/OPENAI_API_KEY/); | ||
| expect(out.warnings.join(' ')).toMatch(/ORCAROUTER_API_KEY/); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clear ORCAROUTER_API_KEY in this fixture.
The beforeEach cleanup does not remove ORCAROUTER_API_KEY. If the host environment defines it, the no-provider test selects OrcaRouter and can call the unmocked adapter instead of asserting the no-key warning. Delete this variable with the other provider keys.
Proposed fix
delete process.env.GROQ_API_KEY;
+ delete process.env.ORCAROUTER_API_KEY;
delete process.env.WIGOLO_LLM_PROVIDER;🤖 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/unit/extraction/llm-fallback.test.ts` at line 73, Update the beforeEach
cleanup in the llm-fallback test fixture to delete ORCAROUTER_API_KEY alongside
the other provider keys, ensuring the no-provider test cannot inherit the host
environment value.
| it('uses the OrcaRouter base URL and json_object response_format', async () => { | ||
| create.mockResolvedValue({ | ||
| choices: [{ message: { content: '{"price":"$1"}' } }], | ||
| model: 'm', | ||
| }); | ||
| await callOrcaRouter({ prompt: 'p', jsonSchema: schema }, 'k'); | ||
| expect(create.mock.calls[0][0].response_format).toEqual({ | ||
| type: 'json_object', | ||
| }); | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Assert the OrcaRouter base URL in both adapter test suites.
The mocked OpenAI client currently discards constructor options, so these tests do not verify that requests are sent to https://api.orcarouter.ai/v1. Capture the constructor arguments and assert the exact baseURL in this test and the corresponding vision tests.
📍 Affects 2 files
tests/unit/extraction/llm/orcarouter.test.ts#L31-L40(this comment)tests/unit/integrations/cloud/llm/text-adapters-vision.test.ts#L117-L135
🤖 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/unit/extraction/llm/orcarouter.test.ts` around lines 31 - 40, Update
the test around callOrcaRouter to capture the arguments passed to the OpenAI
constructor rather than relying on the mocked client instance. Assert that the
constructor options include baseURL set to https://api.orcarouter.ai/v1, while
preserving the existing response_format assertion.
Apply the same fix in
`@tests/unit/integrations/cloud/llm/text-adapters-vision.test.ts` around lines 117
- 135: Same missing baseURL assertion for the text vision adapter.
What & why
Adds OrcaRouter as a named provider. With one API key, users get 150+ models from OpenAI, Anthropic, Google, DeepSeek, Qwen, MiniMax and xAI behind a single endpoint. Because the LLM provider layer is a thin base-URL swap, any wigolo pipeline/agent that uses it also inherits OrcaRouter's gateway-level, zero-trust security controls for AI agents — with no application code changes. The gateway screens every prompt and response and governs every tool call on a default-deny basis, across four layers:
I'm an engineer on the OrcaRouter team.
Changes
orcarouteras a first-class LLM provider: auto-detect order,ORCAROUTER_API_KEY, model default (orcarouter/auto) plusWIGOLO_LLM_MODEL_ORCAROUTERcallOrcaRouterTexttext adapter (OpenAI-compatible, vision via data: URL) for research/agent synthesiscallOrcaRouterstructured-extraction adapter (json_object + client-side schema validation + one retry) for extract's LLM fallback--providervalidation, config wizard entry, keystore storage (env-only in the picker, mirroring groq)Testing
npm run lintpasses (tsc --noEmitclean)npm run buildsucceedsmainin this environmenthttps://api.orcarouter.ai/v1withorcarouter/auto: text synthesis, JSON extraction, and the structured adapter all returned valid completionsChecklist
CONTRIBUTING.mdand agree to its contribution termsSummary by CodeRabbit
New Features
Documentation
Tests