Skip to content

feat: add OrcaRouter as a named LLM provider - #302

Open
XiaoHuo888-hue wants to merge 1 commit into
KnockOutEZ:mainfrom
XiaoHuo888-hue:feat/orcarouter-provider
Open

feat: add OrcaRouter as a named LLM provider#302
XiaoHuo888-hue wants to merge 1 commit into
KnockOutEZ:mainfrom
XiaoHuo888-hue:feat/orcarouter-provider

Conversation

@XiaoHuo888-hue

@XiaoHuo888-hue XiaoHuo888-hue commented Aug 12, 2026

Copy link
Copy Markdown

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:

  • Scoped keys — bind a key to specific models, IPs, spend caps, and expiry.
  • Guardrails — screen for PII, secret leakage, prompt injection, and unsafe output.
  • Agent firewall — tool allow-lists with per-argument validation.
  • Audit trail — a record of every match, verdict, and approval decision.

I'm an engineer on the OrcaRouter team.

Changes

  • Register orcarouter as a first-class LLM provider: auto-detect order, ORCAROUTER_API_KEY, model default (orcarouter/auto) plus WIGOLO_LLM_MODEL_ORCAROUTER
  • callOrcaRouterText text adapter (OpenAI-compatible, vision via data: URL) for research/agent synthesis
  • callOrcaRouter structured-extraction adapter (json_object + client-side schema validation + one retry) for extract's LLM fallback
  • Keystore/init/TUI wiring: --provider validation, config wizard entry, keystore storage (env-only in the picker, mirroring groq)
  • Vision gate accepts orcarouter
  • Tests + docs updated

Testing

  • npm run lint passes (tsc --noEmit clean)
  • npm run build succeeds
  • Unit tests: 7640 passed; the only failures are 18 pre-existing platform-only failures in symlink/tty/EACCES tests that also fail on main in this environment
  • New tests cover provider selection, the text adapter, the structured adapter (retry on schema mismatch), and the full fallback path
  • Live check against https://api.orcarouter.ai/v1 with orcarouter/auto: text synthesis, JSON extraction, and the structured adapter all returned valid completions

Checklist

  • Follows Conventional Commits
  • Focused change (no unrelated refactoring)
  • I have read CONTRIBUTING.md and agree to its contribution terms

Summary by CodeRabbit

  • New Features

    • Added OrcaRouter as a supported LLM provider.
    • Added OpenAI-compatible text, vision, and structured extraction support.
    • Added automatic provider selection using OrcaRouter configuration.
    • Added OrcaRouter API-key storage, initialization, and interactive setup support.
  • Documentation

    • Updated configuration and setup guidance with OrcaRouter options and API-key settings.
  • Tests

    • Added coverage for OrcaRouter requests, validation retries, multimodal inputs, and provider selection.

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>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

OrcaRouter provider support

Layer / File(s) Summary
Provider contract and selection
src/integrations/cloud/llm/types.ts, src/integrations/cloud/llm/select.ts, src/integrations/cloud/llm/model-select.ts, src/extraction/llm-fallback.ts, src/integrations/cloud/llm/run.ts
Adds orcarouter to provider types, selection, model defaults, API-key mapping, and extraction dispatch.
OrcaRouter extraction and text adapters
src/integrations/cloud/llm/orcarouter.ts, src/integrations/cloud/llm/text-adapters.ts
Adds schema-constrained extraction and OpenAI-compatible text and multimodal requests with validation, retry handling, abort support, and result metadata.
CLI, key storage, and vision wiring
src/cli/..., src/security/key-store.ts, src/fetch/browser-pool.ts
Adds OrcaRouter to CLI provider choices, accepted flags, keystore discovery, provider-key messages, and vision-provider detection.
Documentation and validation
README.md, docs/configuration.md, tests/...
Documents OrcaRouter and validates extraction, provider selection, text adapters, vision payloads, runner registration, and environment cleanup.

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

Mergeability Score: 🟡 Moderate · up to 38e46

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
Loading

Possibly related PRs

  • KnockOutEZ/wigolo#254: Adds another authenticated cloud LLM provider across shared adapter, selection, model, keystore, and extraction paths.
  • KnockOutEZ/wigolo#260: Extends the shared text adapter path for multimodal vision requests.
  • KnockOutEZ/wigolo#276: Adds an OpenAI-compatible provider across provider selection, adapters, models, keystore, CLI, and types.

Suggested reviewers: knockoutez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding OrcaRouter as a named LLM provider.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Warning

⚠️ This pull request has been flagged as potential spam (promotional) by CodeRabbit slop detection and should be reviewed carefully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b3ccf92 and 38e46cc.

📒 Files selected for processing (30)
  • README.md
  • docs/configuration.md
  • src/cli/init.ts
  • src/cli/tui/actions/verify-e2e.ts
  • src/cli/tui/extras-prompt.ts
  • src/cli/tui/flags.ts
  • src/cli/tui/schema/llm.ts
  • src/extraction/llm-fallback.ts
  • src/fetch/browser-pool.ts
  • src/integrations/cloud/llm/model-select.ts
  • src/integrations/cloud/llm/orcarouter.ts
  • src/integrations/cloud/llm/run.ts
  • src/integrations/cloud/llm/select.ts
  • src/integrations/cloud/llm/text-adapters.ts
  • src/integrations/cloud/llm/types.ts
  • src/security/key-store.ts
  • tests/integration/llm-fallback-e2e.test.ts
  • tests/integration/zero-touch-just-works.test.ts
  • tests/unit/agent/pipeline.llm-gate-keystore.test.ts
  • tests/unit/cli/tui/actions/provider-actions.test.ts
  • tests/unit/cli/tui/schema/llm-no-custom.test.ts
  • tests/unit/cli/tui/schema/llm.test.ts
  • tests/unit/extraction/llm-fallback.test.ts
  • tests/unit/extraction/llm/orcarouter.test.ts
  • tests/unit/extraction/llm/select.test.ts
  • tests/unit/extraction/llm/types.test.ts
  • tests/unit/integrations/cloud/llm/text-adapters-vision.test.ts
  • tests/unit/integrations/llm-runner.test.ts
  • tests/unit/research/pipeline.llm-gate-keystore.test.ts
  • tests/unit/research/synthesis-local.test.ts

Comment thread docs/configuration.md
Comment on lines +102 to +103
| `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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 tests

Repository: 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.ts

Repository: 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
PY

Repository: 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")
PY

Repository: 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.

Comment on lines +168 to +180
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +31 to +40
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',
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

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