Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,15 @@ APP_LOG_TO_FILE=true
# FREE_PROXY_IPLOCATE_ENABLED=false
# FREE_PROXY_IPLOCATE_BASE_URL=https://raw.githubusercontent.com/iplocate/free-proxy-list/main/protocols

# ── Free Proxy Pool (Webshare source) ──
# Used by: src/lib/freeProxyProviders/webshare.ts
# Paid, per-account proxy list — requires FREE_PROXY_WEBSHARE_API_KEY to activate,
# regardless of FREE_PROXY_WEBSHARE_ENABLED.
# FREE_PROXY_WEBSHARE_ENABLED=true
# FREE_PROXY_WEBSHARE_API_KEY=
# FREE_PROXY_WEBSHARE_API_URL=https://proxy.webshare.io/api/v2/proxy/list/
# FREE_PROXY_WEBSHARE_MAX=500

# ── Vercel Relay ──
# Used by: src/app/api/settings/proxy/vercel-deploy/route.ts
# Hides the "Deploy Relay" button when set to false.
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@
- **hygiene:** Add CodeRabbit review config (.coderabbit.yaml) and Gemini context (.gemini/config.json, .gemini/prompts.md)
- **hygiene:** Add ADR.md with fleet hygiene decisions (ADR-001 baseline, ADR-002 e2e restoration, ADR-003 dual dependency automation)
- **e2e:** Remove orphaned testIgnore entries (analytics-tabs, protocol-visibility, skills-marketplace) whose specs no longer exist
- **feat(server):** support reverse-proxy subpath deployment via OMNIROUTE_BASE_PATH (basePath-aware auth redirects). (thanks @SillyHippy)
- **feat(api-keys):** track devices/connections per API key — an in-memory, TTL-evicted device fingerprint tracker (SHA-256 of masked IP + truncated user-agent) wired non-blocking into the chat path and surfaced via `GET /api/keys/[id]/devices` with a dashboard device-count chip. (thanks @mugnimaestra)
- **feat(proxy):** add Webshare proxy pool import and sync — a `WebshareProvider` (`FreeProxyProvider`) that paginates `proxy.webshare.io/api/v2/proxy/list/` gated on `FREE_PROXY_WEBSHARE_API_KEY`, SSRF-guards imported hosts, and tombstones retired proxy IDs via `pruneStaleFreeProxies()`. (thanks @ricatix)
- **feat(dashboard):** suggest HuggingFace Hub media models in the media provider view. (thanks @yicone)

### 🔧 Bug Fixes

- **ci:** Pin scorecard workflow to ubuntu-24.04

### 📝 Maintenance

- **API validation:** add a `validatedJsonBody(request, schema)` helper in `src/shared/validation/helpers.ts` that fuses JSON body parsing and Zod validation into a single call, returning either the type-narrowed data or a ready-to-return 400 `NextResponse` with the standard error envelope. Salvaged from the closed refactor PR #5075 (Tier 1 portable helper) with a focused 6-case regression test. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>

---

## [3.8.43] — 2026-07-02
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,10 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
| `FREE_PROXY_PROXIFLY_ANONYMITY` | `elite` | `src/lib/freeProxyProviders/proxifly.ts` | Anonymity level filter for Proxifly (`elite`, `anonymous`, `transparent`). |
| `FREE_PROXY_IPLOCATE_ENABLED` | `false` | `src/lib/freeProxyProviders/iplocate.ts` | Enable the IPLocate free proxy source. Opt-in only. |
| `FREE_PROXY_IPLOCATE_BASE_URL` | `https://raw.githubusercontent.com/iplocate/free-proxy-list/main/protocols` | `src/lib/freeProxyProviders/iplocate.ts` | IPLocate proxy list base URL override. |
| `FREE_PROXY_WEBSHARE_ENABLED` | `true` | `src/lib/freeProxyProviders/webshare.ts` | Enable the Webshare proxy pool source. Set to `false` to disable; also requires `FREE_PROXY_WEBSHARE_API_KEY` to be set. |
| `FREE_PROXY_WEBSHARE_API_KEY` | _(none)_ | `src/lib/freeProxyProviders/webshare.ts` | Webshare account API token (`Authorization: Token <key>`). Required — the provider stays disabled without it. |
| `FREE_PROXY_WEBSHARE_API_URL` | `https://proxy.webshare.io/api/v2/proxy/list/` | `src/lib/freeProxyProviders/webshare.ts` | Webshare proxy list API URL override. |
| `FREE_PROXY_WEBSHARE_MAX` | `500` | `src/lib/freeProxyProviders/webshare.ts` | Maximum proxies imported per Webshare sync. |
| `NEXT_PUBLIC_VERCEL_RELAY_ENABLED` | `true` | `src/app/(dashboard)/…/ProxyPoolTab.tsx` | Show/hide the Deploy Vercel Relay button in the Proxy Pool tab. |
| `VERCEL_API_BASE` | `https://api.vercel.com` | `src/app/api/settings/proxy/vercel-deploy/route.ts` | Vercel API base URL override (for testing). |
| `NEXT_PUBLIC_VERCEL_RELAY_DEFAULT_PROJECT` | `omniroute-relay` | `src/app/(dashboard)/…/VercelRelayModal.tsx` | Default project name pre-filled in the Vercel Relay deploy modal. |
Expand Down
7 changes: 7 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ function readTimeoutMs(...values) {

/** @type {import('next').NextConfig} */
const nextConfig = {
// Opt-in subpath deployment behind a reverse proxy (e.g. nginx/Caddy serving
// OmniRoute under https://host/omniroute/). Empty by default so root-path
// deployments are unaffected. Next.js strips this prefix from `pathname`
// before route matching, so authz classification (classifyRoute/isLocalOnlyPath)
// keeps operating on un-prefixed paths — see src/server/authz/pipeline.ts for
// the two redirect call sites that re-add it via `request.nextUrl.basePath`.
basePath: process.env.OMNIROUTE_BASE_PATH || "",
distDir,
// Turbopack config: redirect native modules to stubs at build time
turbopack: {
Expand Down
21 changes: 21 additions & 0 deletions open-sse/config/imageRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,27 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
models: [{ id: "sensenova-u1-fast", name: "SenseNova U1 Fast" }],
supportedSizes: ["1024x1024"],
},

// HuggingFace Hub Inference API text-to-image task. Returns raw image bytes
// (not JSON), so it uses a dedicated "huggingface-image" format handled by
// handleHuggingFaceImageGeneration. Same base URL convention as the HF
// STT/TTS entries in audioRegistry.ts. Model list is deliberately small —
// the dashboard's "suggested models" chip row (GET
// /api/v1/providers/suggested-models) surfaces additional HF Hub models
// beyond this seed list.
huggingface: {
id: "huggingface",
baseUrl: "https://api-inference.huggingface.co/models",
authType: "apikey",
authHeader: "bearer",
format: "huggingface-image",
models: [
{ id: "black-forest-labs/FLUX.1-dev", name: "FLUX.1 Dev (HF)" },
{ id: "black-forest-labs/FLUX.1-schnell", name: "FLUX.1 Schnell (HF)" },
{ id: "stabilityai/stable-diffusion-xl-base-1.0", name: "Stable Diffusion XL (HF)" },
],
supportedSizes: ["1024x1024"],
},
};

/**
Expand Down
10 changes: 10 additions & 0 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
isStripReasoningRequested,
} from "./chatCore/headers.ts";
import { markCodexScopeRateLimited } from "./chatCore/codexFailover.ts";
import { trackDevice, extractIpFromHeaders } from "../services/deviceTracker.ts";
import { getCombosCached } from "./chatCore/comboContextCache.ts";
export { clearCombosCache, clearUpstreamProxyConfigCache } from "./chatCore/comboContextCache.ts";
import {
Expand Down Expand Up @@ -454,6 +455,15 @@ export async function handleChatCore({
if (pluginGate.body) {
body = pluginGate.body;
}
// Per-API-key device/connection tracking (port of upstream 9router#931,
// thanks @mugnimaestra). In-memory only, never blocks the request path.
if (apiKeyInfo?.id) {
trackDevice(
apiKeyInfo.id,
extractIpFromHeaders(clientRawRequest?.headers ?? null),
userAgent ?? null
);
}
const agentGoalPolicy = resolveAgentGoalPolicy(body, clientRawRequest?.headers ?? null);
if (agentGoalPolicy.detected) {
log?.debug?.(
Expand Down
12 changes: 12 additions & 0 deletions open-sse/handlers/imageGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { sanitizeErrorMessage, sanitizeUpstreamDetails } from "../utils/error.ts
// are still used by handleImageEdit below, so they are imported (not re-defined).
import { handleSDWebUIImageGeneration } from "./imageGeneration/providers/sdWebUI.ts";
import { handleHyperbolicImageGeneration } from "./imageGeneration/providers/hyperbolic.ts";
import { handleHuggingFaceImageGeneration } from "./imageGeneration/providers/huggingface.ts";
import { handleComfyUIImageGeneration } from "./imageGeneration/providers/comfyUI.ts";
import { handleImagen3ImageGeneration } from "./imageGeneration/providers/imagen3.ts";
import { handleIdeogramImageGeneration } from "./imageGeneration/providers/ideogram.ts";
Expand Down Expand Up @@ -379,6 +380,17 @@ export async function handleImageGeneration({
});
}

if (providerConfig.format === "huggingface-image") {
return handleHuggingFaceImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
});
}

if (providerConfig.format === "fal-ai") {
return handleFalAIImageGeneration({
model,
Expand Down
90 changes: 90 additions & 0 deletions open-sse/handlers/imageGeneration/providers/huggingface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// HuggingFace Hub image-generation provider.
//
// The HF Inference API text-to-image task returns the generated image as raw
// binary bytes (e.g. `image/jpeg`), not a JSON envelope — unlike most other
// image providers wired in this file. Mirrors the shape/error-handling
// conventions used by ./hyperbolic.ts and ./leonardo.ts.

import { saveCallLog } from "@/lib/usageDb";
import { sanitizeErrorMessage } from "../../../utils/error.ts";

export async function handleHuggingFaceImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const startTime = Date.now();
const token = credentials?.apiKey || credentials?.accessToken || "";
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");

if (log) {
log.info("IMAGE", `${provider}/${model} (huggingface) | prompt: "${prompt.slice(0, 60)}..."`);
}

try {
const response = await fetch(`${providerConfig.baseUrl}/${model}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ inputs: prompt }),
});

if (!response.ok) {
const errorText = await response.text();
if (log)
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);

saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: response.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: errorText.slice(0, 500),
}).catch(() => {});

return { success: false, status: response.status, error: errorText };
}

const buf = await response.arrayBuffer();

saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
}).catch(() => {});

return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: [{ b64_json: Buffer.from(buf).toString("base64"), revised_prompt: prompt }],
},
};
} catch (err) {
if (log) log.error("IMAGE", `${provider} fetch error: ${err.message}`);
saveCallLog({
method: "POST",
path: "/v1/images/generations",
status: 502,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: err.message,
}).catch(() => {});
return {
success: false,
status: 502,
error: `Image provider error: ${sanitizeErrorMessage((err as Error).message || err)}`,
};
}
}
Loading
Loading