Support OpenRouter-compatible relay base URLs across UI, API, and CLI#124
Support OpenRouter-compatible relay base URLs across UI, API, and CLI#124xieli123-hub wants to merge 1 commit intoOpenLAIR:mainfrom
Conversation
Zhang-Henry
left a comment
There was a problem hiding this comment.
Code Review
Clean central architecture — openrouterConfig.js normalizes base URLs and conditions provider-specific headers across all three callsites. The utility design is solid and well-tested. However, there are real issues to address before merging.
Blocking
1. SSRF via user-controlled baseUrl in the agent API route
server/routes/agent.js: The baseUrl from req.body is passed directly to queryOpenRouter, which uses it as the target of a fetch. Unlike /openrouter/verify-api-key (which validates via normalizeOpenRouterBaseUrl), the agent route performs no validation. A caller with a valid API key can direct the server to fetch any internal URL (http://169.254.169.254/, http://localhost:3000/admin, etc.) and have the response streamed back via WebSocket.
Fix: validate that the hostname is not a private/loopback/link-local address, or restrict baseUrl to a server-side env setting only.
Should Fix
2. Default base URL always written to .env on key save
server/routes/cli-auth.js (~L603): normalizeOpenRouterBaseUrl returns the official URL when req.body.baseUrl is undefined/empty. The code then unconditionally writes OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 to .env and sets process.env.OPENROUTER_BASE_URL. Users who save an API key without filling in a relay URL get a surprise .env entry. Fix: only write/set OPENROUTER_BASE_URL when the caller explicitly supplied a non-empty value.
3. agentLoop uses baseUrl via closure, not parameter
server/cli-chat.js (~L412): agentLoop references baseUrl as a closure variable from startChat. If the function is ever called from elsewhere or moved, baseUrl will be undefined. The signature should explicitly receive baseUrl as a parameter.
4. .env parser is fragile
server/routes/cli-auth.js, upsertEnvValues: .split('=')[0] works for simple cases but doesn't handle edge cases like whitespace-prefixed keys ( OPENROUTER_API_KEY=...). Consider using a proper .env line-parse regex or a library like dotenv-edit.
Minor
- Raw fetch error messages are echoed to the API caller in the catch block (~L631) — could leak internal URL info if a relay returns verbose errors. Consider sanitizing.
- The
filterlogic for trailing blank lines inupsertEnvValuesdoesn't de-duplicate consecutive blank lines in the middle of the file. - Removing the module-level
_modelsCacheinProviderSelectionEmptyState.tsxmeans every remount triggers an API call to/api/settings/openrouter-models. The server-side 30-minute cache absorbs this, but worth noting.
Positive Notes
- The
openrouterConfig.jsseparation of concerns (normalizeOpenRouterBaseUrlthrows on bad input,getOpenRouterBaseUrlis safe/fallback,isOfficialOpenRouterBaseUrl,getOpenRouterProviderHeaders) is clean and testable. isDirectExecutionguard inserver/cli.jsis the right approach for ESM test imports.closeDatabaseaddition todb.jsand its use in tests is proper cleanup.- Model-list API fallback for flat JSON arrays vs
{ data: [...] }is good defensive coding. - Test coverage for
openrouterConfig.jsandcliArgs.jsis thorough.
Summary
This PR adds first-class support for OpenRouter-compatible relay / proxy endpoints so Dr. Claw can work with relay-based APIs without assuming the official OpenRouter base URL everywhere.
What Changed
Verification
pm.cmd run typecheck
pm.cmd run test -- server/tests/openrouter-config.test.mjs server/tests/cli.test.mjs
????