feat: universal LLM provider gateway with pluggable search - #42
Conversation
…ggable search New LLM providers: OpenRouterLLM and OpenAICompatibleLLM extend BaseLLM. Tavily search wrapper for providers without native web search. BaseLLM gains search tool injection, parametric knowledge fallback for no-tools case, tool-calling error recovery, and Pass 2 retry with cached search results to avoid burning Tavily credits on retries. Existing providers (Anthropic, OpenAI, Gemini) unchanged when no search tools are injected.
Factory gains search tool resolution (native/tavily/none/auto-detect), new LLM branches for openrouter and openai-compatible, and updated createVerificationModel with KEY_MAP lookup. ProviderConfig expanded with llmModel, llmBaseUrl, and searchProvider fields.
CLI gains openrouter and openai-compatible provider choices, model/URL overrides, and search provider selection. Env validation adds OPENROUTER_API_KEY and TAVILY_API_KEY requirements. Cost estimator uses separate LLM_PRICING lookup with zero-cost fallback for unknown providers.
…and web UI Server exposes new provider options and search provider list. Worker threads llmModel, llmBaseUrl, searchProvider through BullMQ job data. Web UI adds OpenRouter and OpenAI-compatible dropdowns with conditional model/URL inputs and search provider selector.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@claude review |
|
Claude finished @tsensei's task in 2m 13s —— View job Code Review
This is a well-structured PR with clear separation of concerns. The pluggable search design is clean, the resilience additions are thoughtful, and the test coverage is solid. A few issues worth addressing before merge: Medium Issues1. Silent fallback in
case "openai-compatible": {
const baseUrl = process.env["OPENREELS_LLM_BASE_URL"];
if (!baseUrl) {
// Fall back to Anthropic for verification if no base URL configured
const anthropic = createAnthropic(); // ← needs ANTHROPIC_API_KEY
return anthropic(model ?? "claude-sonnet-4-6");
}A user running Ollama (no Anthropic key) will hit this path and get a runtime auth failure on stock verification, not a clear error at startup. The fallback should either throw or at least log a warning: if (!baseUrl) {
throw new Error(
"OPENREELS_LLM_BASE_URL must be set for openai-compatible verification model"
);
}2. Overly broad string matching for tool-calling error fallback
if (
msg.includes("tool") || // ← matches "timeout occurred during tooling"
msg.includes("Tool") ||
msg.includes("function_call") ||
msg.includes("tools_not_supported")
) {
3. Duplicate "no Tavily key" warning Both 4.
interface CreateJobBody {
providers?: {
// ...
searchProvider?: string; // ← should be SearchProviderKey
};
}This bypasses type safety all the way to the worker cast. Import Minor Issues5. Hardcoded
6. Web UI missing client-side validation for openai-compatible
7. Non-null assertion
throw lastError ?? new Error(`${this.id} Pass 2 failed after ${MAX_STRUCTURE_RETRIES + 1} attempts`);Positive Notes
Overall this is merge-ready pending the medium issues (#1 silent Anthropic fallback, #2 broad error matching, #3 duplicate warning, #4 string vs SearchProviderKey). |
- createVerificationModel: throw clear error when openai-compatible
lacks both OPENREELS_LLM_BASE_URL and ANTHROPIC_API_KEY, log warning
when falling back to Anthropic for VLM verification
- base.ts: tighten tool-calling error detection from broad
includes("tool") to specific patterns (tools_not_supported,
tool_use, tool_calls, etc.), log original error message on fallback
- server.ts/worker.ts: type searchProvider as SearchProviderKey
instead of string
Code review responsePushed Fixed#1 — Silent Anthropic fallback in #2 — Broad tool-error string matching ✅ #4 — Not fixing (by design)#3 — Duplicate "no Tavily key" warning #5 — Hardcoded #6 — Web UI missing client-side validation for openai-compatible #7 — Non-null assertion |
|
@claude review |
|
Claude finished @tsensei's task in 2m 54s —— View job Code Review
All three targeted fixes from the previous review are correct. I found one new medium bug introduced by the #1 fix, plus a few minor nits. Previous fixes verified ✅#1 — #2 — Broad tool error matching ✅ Fixed. The new patterns ( #4 — New Medium IssueBYOK path passes the wrong API key to the Anthropic fallback
const antKey = apiKey ?? process.env["ANTHROPIC_API_KEY"];In the BYOK worker path ( const LLM_KEY_MAP = { "openai-compatible": "OPENREELS_LLM_API_KEY", ... };
const llmKey = keys[llmKeyName]; // ← the openai-compatible endpoint key
const verifyModel = createVerificationModel(provider, model, llmKey);So The CLI path ( Fix — ignore case "openai-compatible": {
const baseUrl = process.env["OPENREELS_LLM_BASE_URL"];
if (!baseUrl) {
const antKey = process.env["ANTHROPIC_API_KEY"]; // ← don't use apiKey here
if (!antKey) {
throw new Error(
"Stock verification requires a VLM. Set OPENREELS_LLM_BASE_URL or ANTHROPIC_API_KEY, or use --no-stock-verify.",
);
}
console.warn(`[openai-compatible] Using Anthropic for stock verification (VLM)...`);
const anthropic = createAnthropic({ apiKey: antKey });
return anthropic(model ?? "claude-sonnet-4-6");
}Minor IssuesRedundant Now that searchProvider: providers.searchProvider as
| import("./schema/providers.js").SearchProviderKey
| undefined,Can be simplified to just
const DISPLAY_NAMES: Record<string, string> = {
// add these:
openrouter: "OpenRouter",
"openai-compatible": "Custom (OpenAI-compatible)",
// remove this:
ollama: "Ollama (Local)", // ← dead, no longer a valid keyPositive notes
One medium bug to fix (wrong key in BYOK Anthropic fallback), two minor nits (redundant cast, |
- factory.ts: don't use apiKey (openai-compatible endpoint key) when falling back to Anthropic for VLM verification; use env var only - worker.ts: remove redundant SearchProviderKey cast now that the type is correct on JobData - HomePage.tsx: add openrouter and openai-compatible to DISPLAY_NAMES, remove dead ollama entry
Round 2 review responseAll three findings valid. Pushed BYOK Anthropic fallback (medium) — Good catch. In the worker path, Redundant SearchProviderKey cast — Removed. The type fix from round 1 made it dead code. DISPLAY_NAMES gaps — Added |
|
@claude review |
|
Claude finished @tsensei's task in 2m 35s —— View job Code Review (Round 3)
All three round 2 fixes are correct. No new medium or high issues found. Full verification below. Round 2 fixes verified ✅BYOK Anthropic fallback ✅ Fixed correctly. Redundant
Final sweep — nothing blockingOne nit:
return compat(model ?? "gpt-4o");When The TODOS.md P2 item (model capability detection) would eventually address this, but in the short term a log hint — "use Positive notes
This is ready to merge. No new issues introduced by commit |
- createVerificationModel: throw clear error when openai-compatible
lacks both OPENREELS_LLM_BASE_URL and ANTHROPIC_API_KEY, log warning
when falling back to Anthropic for VLM verification
- base.ts: tighten tool-calling error detection from broad
includes("tool") to specific patterns (tools_not_supported,
tool_use, tool_calls, etc.), log original error message on fallback
- server.ts/worker.ts: type searchProvider as SearchProviderKey
instead of string
- Add console.warn when negativePrompt is silently dropped by Gemini's veo-3.1-lite model (review finding #1, valid)
- Add console.warn when negativePrompt is silently dropped by Gemini's veo-3.1-lite model (review finding #1, valid)
Summary
Universal provider gateway — use any OpenAI-compatible LLM endpoint with OpenReels. Independently configurable search backends.
Providers:
--provider openrouter— 300+ models via OpenRouter (default model:anthropic/claude-sonnet-4)--provider openai-compatible --llm-base-url <url> --llm-model <model>— Ollama, Together, Groq, vLLM, any OpenAI-compatible endpoint--llm-model <model>— override model for any provider (including existing ones)Search:
--search-provider native— use provider's built-in search (Anthropic, OpenAI, Gemini)--search-provider tavily— Tavily web search for any provider--search-provider none— parametric knowledge only (single-pass, half token cost)TAVILY_API_KEYis setResilience:
Web UI: OpenRouter and Custom (OpenAI-compatible) in LLM dropdown, conditional model/URL inputs, search provider selector.
Test Coverage
Tests: 349 → 386 (+37 new)
All new code paths covered:
Pre-Landing Review
No issues found. Enum completeness verified: all 5 LLMProviderKey values handled in every consumer (factory, cost estimator, worker key map, validation).
TODOS
Test plan
tsc --noEmit)--provider openrouter --llm-model google/gemini-2.5-flash --search-provider tavilyruns full pipeline--provider anthropicunchangedDocumentation
9 files updated:
🤖 Generated with Claude Code