Skip to content

fix(models): proxy custom OpenAI-compatible models through backend to bypass CORS#723

Open
ital0 wants to merge 28 commits intomainfrom
italomenezes/thu-424-cors-prevents-custom-models-in-web
Open

fix(models): proxy custom OpenAI-compatible models through backend to bypass CORS#723
ital0 wants to merge 28 commits intomainfrom
italomenezes/thu-424-cors-prevents-custom-models-in-web

Conversation

@ital0
Copy link
Copy Markdown
Collaborator

@ital0 ital0 commented Apr 22, 2026

Problem

Web app cannot add custom OpenAI-compatible model endpoints — browser CORS blocks GET {url}/v1/models and POST {url}/v1/chat/completions. Tauri desktop/mobile already bypass via native fetch.

Solution

Two new backend routes proxy cloud URLs through the existing SSRF-hardened createSafeFetch (backend/src/utils/url-validation.ts). Localhost URLs keep direct-browser fetch (backend can't reach user loopback). Tauri path unchanged.

  • POST /v1/custom-model/proxy — chat completions, streaming via OpenAI SDK.
  • POST /v1/custom-model/models — model discovery.

Why createSafeFetch instead of undici.Agent: Phase-1.5 spike proved Bun 1.3.10 silently ignores undici's connect hook. createSafeFetch is already production-tested (pro/proxy.ts, pro/link-preview.ts, mcp-proxy/routes.ts) and uses the same ipaddr.js denylist. Zero new npm deps.

Security

SSRF defense via createSafeFetch (RFC-1918, loopback, link-local incl. 169.254.169.254, CGNAT, IPv4-mapped IPv6, etc). Per-user rate limit 60 req/min. Content-Type gate (application/json | text/event-stream). 101 Switching Protocols → 502. 50 MB total byte cap. Auth required; upstreamAuth redacted at pino level and kept in body (never query string or Authorization header on the browser→backend leg). Outbound User-Agent: Thunderbolt-Proxy/1.0 + X-Abuse-Contact.

Changes

Area File Notes
Shared types shared/custom-model-proxy.ts 47 LoC, pure types
Backend proxy backend/src/inference/custom-model-proxy.ts two Elysia routes
Backend client backend/src/inference/client.ts getCustomModelClient factory
Backend config backend/src/config/logger.ts pino redact for upstreamAuth
Frontend fetch src/ai/fetch.ts, src/ai/custom-proxy-fetch.ts routes chat completions via proxy for cloud URLs (injectable tauriFetch for tests)
Frontend UI src/settings/models/index.tsx Add Model dialog routes through proxy + maps error codes
Helper src/ai/is-localhost-url.ts shared localhost check

Test plan

  • bun tsc --noEmit clean (backend + frontend, apart from pre-existing bun:test noise on main)
  • bun test src/lib/http.test.ts — 13/13
  • bun test src/services/encryption.test.ts — 16/16
  • bun test src/ai/ — 210/210
  • bun test src/settings/models/ — 2/2
  • bun test backend/src/inference/ — 32/32
  • SSRF denylist covered by existing url-validation.test.ts
  • Manual: cloud custom model → chat end-to-end
  • Manual: Ollama (localhost) custom model → direct path
  • Manual: Tauri desktop unchanged

Note

High Risk
Adds new authenticated proxy endpoints that forward user-supplied URLs and API keys and perform outbound network requests/streaming, which is inherently SSRF- and data-exfiltration-sensitive despite added guards (safe fetch, validation, rate limits, redaction). Failures or gaps in validation/limits could impact security or availability.

Overview
Enables web usage of custom OpenAI-compatible model endpoints by routing model discovery and chat-completions through new authenticated backend proxy routes (POST /v1/custom-model/models and POST /v1/custom-model/proxy) to avoid browser CORS.

The proxy layer adds SSRF-focused validation, per-user rate limiting, content-type checks, protocol-upgrade blocking, request timeouts, and response size caps, and forces outbound User-Agent/X-Abuse-Contact headers; logging is updated to redact authorization/apiKey/upstreamAuth.

Frontend model creation/testing now uses a createCustomProxyFetch wrapper to send cloud requests via the backend (keeping upstream keys out of browser→third-party traffic), while localhost/loopback URLs still use direct fetch; the Models UI routes custom /models fetching via the proxy and maps proxy error codes to user-friendly messages, with added shared wire types and tests.

Reviewed by Cursor Bugbot for commit a6e8bc0. Bugbot is set up for automated code reviews on this repo. Configure here.

ital0 added 10 commits April 22, 2026 10:56
Add createCustomProxyFetch factory and isLocalhostUrl helper.

- Cloud URLs POST to /v1/custom-model/proxy via authenticated HttpClient;
  upstreamAuth travels in the request body only (never in Authorization header).
- Localhost/loopback URLs use globalThis.fetch directly (CORS carve-out).
- Tauri runtime delegates to src/lib/fetch (native-fetch path, unchanged).
- createModel accepts optional httpClient and threads it to the custom case.
- 29 tests covering localhost detection, routing decisions, and security invariants.
…end proxy

Fixes CORS-blocked model list fetch for cloud custom URLs by posting to
POST /v1/custom-model/models via the authenticated httpClient. Localhost
URLs keep the existing direct fetch path. Maps ProxyErrorCode values to
user-friendly error messages.
@ital0 ital0 self-assigned this Apr 22, 2026
@github-actions
Copy link
Copy Markdown

Semgrep Security Scan

No security issues found.

Comment thread backend/src/inference/custom-model-proxy.test.ts Fixed
Comment thread backend/src/inference/custom-model-proxy.test.ts Fixed
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

PR Metrics

Metric Value
Lines changed (prod code) +793 / -70
JS bundle size (gzipped) 🟢 1.02 MB → 1.01 MB (-3.5 KB, -0.3%)
Test coverage 🟢 70.64% → 70.74% (+0.1%)
Performance (preview) Preview not ready — Render deploy may have timed out
Accessibility
Best Practices
SEO

Updated Fri, 24 Apr 2026 20:37:26 GMT · run #1201

@ital0 ital0 changed the title fix(models): proxy custom OpenAI-compatible models through backend to bypass CORS (THU-424) fix(models): proxy custom OpenAI-compatible models through backend to bypass CORS Apr 22, 2026
@ital0 ital0 marked this pull request as ready for review April 22, 2026 23:00
Comment thread src/settings/models/index.tsx
Comment thread backend/src/inference/custom-model-proxy.ts Outdated
Comment thread src/settings/models/index.tsx
Comment thread backend/src/inference/custom-model-proxy.ts
Comment thread src/ai/custom-proxy-fetch.ts Outdated
Comment thread src/ai/custom-proxy-fetch.ts
Comment thread src/settings/models/index.tsx Outdated
Comment thread backend/src/inference/custom-model-proxy.ts
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ad596a2. Configure here.

Comment thread src/settings/models/index.tsx
Comment thread backend/src/inference/custom-model-proxy.ts
Comment thread backend/src/inference/custom-model-proxy.ts
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.

2 participants