Feat: Add configurable AI provider base URL flag and update documentation (#3059)

* feat: add FLAG_ALLOW_UNSAFE_AI_BASE_URL for configurable AI provider base URLs

* feat: add FLAG_ALLOW_UNSAFE_AI_BASE_URL documentation

* fix: remove AI_ALLOWED_BASE_URLS from documentation and environment variable reference

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
SirSKillz
2026-05-13 19:01:37 -06:00
committed by GitHub
parent 6c4a4b2aa5
commit c71f3b0b92
8 changed files with 20 additions and 28 deletions
+6
View File
@@ -89,6 +89,12 @@ FLAG_DISABLE_EMAIL_AUTH="false"
# This is useful if you are using a machine with limited resources, like a Raspberry Pi.
FLAG_DISABLE_IMAGE_PROCESSING="false"
# Allows AI providers to be configured with any base URL, including http:// and
# private/loopback addresses (e.g. http://localhost:11434 for a local Ollama instance).
# WARNING: Enabling this on a multi-tenant deployment is a Server-Side Request Forgery (SSRF)
# risk. Only enable this on a trusted, single-tenant self-hosted instance.
FLAG_ALLOW_UNSAFE_AI_BASE_URL="false"
# --- Others ---
# Google Cloud API Key (optional)
# For font-list generation tooling.
+1
View File
@@ -104,3 +104,4 @@ Vitest test paths are package-relative when running through `pnpm --filter <pack
- Biome uses tabs, double quotes, line width 120, organized import groups, and sorted Tailwind classes for `clsx`, `cva`, and `cn`.
- Most packages use `tsgo --noEmit` for typechecking and `vitest run --passWithNoTests` for tests.
- There may be unrelated local edits in the worktree. Inspect `git status --short` first and avoid reverting files you did not touch.
- **New env vars require a `turbo.json` entry.** Turborepo 2.x runs in strict env mode by default — it filters out env vars that are not listed in `globalEnv` (or task-level `env`/`passThroughEnv`). Any new environment variable added to `packages/env/src/server.ts` must also be added to the `globalEnv` array in `turbo.json`, or the variable will be `undefined` inside child processes at runtime even if it is correctly set in the OS/container environment.
@@ -76,17 +76,6 @@ const providerOptions: AIProviderOption[] = [
},
];
function isValidOptionalBaseURL(value: string) {
const trimmedValue = value.trim();
if (!trimmedValue) return true;
try {
return new URL(trimmedValue).protocol === "https:";
} catch {
return false;
}
}
function AIForm() {
const { set, model, apiKey, baseURL, provider, enabled, testStatus } = useAIStore();
@@ -94,7 +83,7 @@ function AIForm() {
return providerOptions.find((option) => option.value === provider);
}, [provider]);
const canTestConnection = model.trim().length > 0 && apiKey.trim().length > 0 && isValidOptionalBaseURL(baseURL);
const canTestConnection = model.trim().length > 0 && apiKey.trim().length > 0;
const { mutate: testConnection, isPending: isTesting } = useMutation(orpc.ai.testConnection.mutationOptions());
+1 -1
View File
@@ -180,7 +180,6 @@ Here's a complete list of environment variables you can configure:
| `BETTER_AUTH_API_KEY` | Better Auth dashboard API key | — |
| `BETTER_AUTH_URL` | Better Auth base URL override (advanced) | `APP_URL` |
| `BETTER_AUTH_SECRET` | Better Auth secret override (advanced) | `AUTH_SECRET` |
| `AI_ALLOWED_BASE_URLS` | Allowlist for custom AI provider base URLs | — |
| `SMTP_HOST` | SMTP Server Host (for email features) | — |
| `SMTP_PORT` | SMTP Server Port | `587` |
| `SMTP_USER` | SMTP Username | — |
@@ -196,6 +195,7 @@ Here's a complete list of environment variables you can configure:
| `FLAG_DISABLE_SIGNUPS` | Disables new user signups | `false` |
| `FLAG_DISABLE_EMAIL_AUTH` | Disables email/password login (SSO only) | `false` |
| `FLAG_DISABLE_IMAGE_PROCESSING` | Disables image processing | `false` |
| `FLAG_ALLOW_UNSAFE_AI_BASE_URL` | Enables local-network AI providers (e.g. Ollama) | `false` |
> **Note:** Some variables are only required for using related features (OAuth, SMTP, S3, etc.) and can be left unset if unused.
+3 -13
View File
@@ -90,10 +90,6 @@ OAUTH_SCOPES=""
# BETTER_AUTH_URL="https://auth.example.com"
# BETTER_AUTH_SECRET=""
# --- AI (optional) ---
# Comma-separated hostnames/origins for custom AI base URLs
# Example: api.openai.com,https://gateway.ai.vercel.com
AI_ALLOWED_BASE_URLS=""
# --- Email (optional) ---
# If all keys are disabled, the app logs the email to be sent to the console instead.
@@ -120,6 +116,7 @@ S3_FORCE_PATH_STYLE="false"
FLAG_DISABLE_SIGNUPS="false"
FLAG_DISABLE_EMAIL_AUTH="false"
FLAG_DISABLE_IMAGE_PROCESSING="false"
FLAG_ALLOW_UNSAFE_AI_BASE_URL="false"
```
</Step>
@@ -254,9 +251,6 @@ docker compose logs -f reactive-resume
<li>
S3 storage (<code>S3_&#42;</code>)
</li>
<li>
AI URL allowlist (<code>AI_ALLOWED_BASE_URLS</code>)
</li>
<li>
Feature flags (<code>FLAG_&#42;</code>)
</li>
@@ -337,15 +331,11 @@ openssl rand -hex 32
- `"false"` for virtual-hosted-style URLs (`https://bucket.endpoint`) common with AWS S3 / Cloudflare R2.
</Accordion>
<Accordion title="AI (optional)">
- **`AI_ALLOWED_BASE_URLS`**: Comma-separated hosts or origins allowed as custom AI API base URLs. - Use this when
routing AI requests through your own gateway/proxy. - Example: `api.openai.com,https://gateway.ai.vercel.com`
</Accordion>
<Accordion title="Feature Flags">
<Accordion title="Feature Flags">
- **`FLAG_DISABLE_SIGNUPS`**: Disables new signups (web app and server). Useful for private instances.
- **`FLAG_DISABLE_EMAIL_AUTH`**: Disables email/password login entirely. Also disables email verification, forgot password, and reset password flows. Users can still sign up via social auth (Google/GitHub/LinkedIn/Custom OAuth), unless FLAG_DISABLE_SIGNUPS is also set to true. Useful when only SSO is required.
- **`FLAG_DISABLE_IMAGE_PROCESSING`**: Disables image processing. This is useful if you are using a machine with limited resources, like a Raspberry Pi.
- **`FLAG_ALLOW_UNSAFE_AI_BASE_URL`**: Allows AI providers to be configured with `http://` URLs and private/loopback addresses (e.g. a local Ollama instance at `http://192.168.1.10:11434`). **Warning: enabling this on a multi-tenant deployment is an SSRF risk.** Only enable on a trusted, single-tenant self-hosted instance.
</Accordion>
</AccordionGroup>
+6 -2
View File
@@ -27,6 +27,7 @@ import {
resumePatchProposalToolOutputSchema,
} from "@reactive-resume/ai/tools/patch-proposal";
import { AI_PROVIDER_DEFAULT_BASE_URLS, aiProviderSchema } from "@reactive-resume/ai/types";
import { env } from "@reactive-resume/env/server";
import { resumeAnalysisOutputSchema, resumeAnalysisSchema } from "@reactive-resume/schema/resume/analysis";
import { applyResumePatches } from "@reactive-resume/utils/resume/patch";
import { isPrivateOrLoopbackHost, parseUrl } from "@reactive-resume/utils/url-security.node";
@@ -79,9 +80,12 @@ function resolveBaseUrl(input: GetModelInput): string {
const parsedBaseURL = parseUrl(baseURL);
if (!parsedBaseURL) throw new Error("INVALID_AI_BASE_URL");
if (parsedBaseURL.protocol !== "https:") throw new Error("INVALID_AI_BASE_URL");
if (parsedBaseURL.username || parsedBaseURL.password) throw new Error("INVALID_AI_BASE_URL");
if (isPrivateOrLoopbackHost(parsedBaseURL.hostname)) throw new Error("INVALID_AI_BASE_URL");
if (!env.FLAG_ALLOW_UNSAFE_AI_BASE_URL) {
if (parsedBaseURL.protocol !== "https:") throw new Error("INVALID_AI_BASE_URL");
if (isPrivateOrLoopbackHost(parsedBaseURL.hostname)) throw new Error("INVALID_AI_BASE_URL");
}
return parsedBaseURL.toString();
}
+1
View File
@@ -70,6 +70,7 @@ export const env = createEnv({
FLAG_DISABLE_SIGNUPS: z.stringbool().default(false),
FLAG_DISABLE_EMAIL_AUTH: z.stringbool().default(false),
FLAG_DISABLE_IMAGE_PROCESSING: z.stringbool().default(false),
FLAG_ALLOW_UNSAFE_AI_BASE_URL: z.stringbool().default(false),
// Crowdin (optional, for translation tooling)
CROWDIN_PROJECT_ID: z.string().optional(),
+1
View File
@@ -37,6 +37,7 @@
"FLAG_DISABLE_SIGNUPS",
"FLAG_DISABLE_EMAIL_AUTH",
"FLAG_DISABLE_IMAGE_PROCESSING",
"FLAG_ALLOW_UNSAFE_AI_BASE_URL",
"GOOGLE_CLOUD_API_KEY",
"CROWDIN_PROJECT_ID",
"CROWDIN_API_TOKEN"