feat(sidecar): advertise provider manifest url (#6007)

* feat(sidecar): advertise provider manifest url via X-OmniRoute-Provider-Manifest-Url header

Re-cut onto release tip: manifest-url feature only (dropped stale-base noise).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* docs(changelog): add sidecar manifest-url entry

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* chore(complexity): rebaseline 2009->2015 (inherited release-tip drift; feature adds 0)

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
This commit is contained in:
KooshaPari
2026-07-02 21:32:45 -07:00
committed by GitHub
parent 6bce788aed
commit 0c2b0571ee
10 changed files with 90 additions and 1 deletions

View File

@@ -418,6 +418,15 @@ NEXT_PUBLIC_BASE_URL=http://localhost:20128
# Do not include /v1; if included accidentally it will be normalized away.
# OMNIROUTE_PUBLIC_BASE_URL=http://192.168.0.15:20128
# Absolute provider plugin manifest URL advertised to sidecar clients.
# Used by: open-sse/config/providerPluginManifestUrl.ts. When unset, OmniRoute
# derives the URL from request origin or HOST/PORT using OMNIROUTE_PUBLIC_PROTOCOL.
# OMNIROUTE_PROVIDER_MANIFEST_URL=https://omniroute.example.com/api/v1/provider-plugin-manifest
# Protocol used when deriving provider plugin manifest URLs without a request origin.
# Used by: open-sse/config/providerPluginManifestUrl.ts. Defaults to http.
# OMNIROUTE_PUBLIC_PROTOCOL=http
# Max wait time for an async chatgpt-web image to land via the celsius
# WebSocket, in milliseconds. Default 180000 (3 minutes). Increase during
# upstream queue-deep windows ("Lots of people are creating images right now").

View File

@@ -11,6 +11,7 @@
- **feat(api):** add `/v1/ocr` endpoint (Mistral OCR), an OCR provider category, and Mistral moderation support. (thanks @waguriagentic)
- **Discovery tool (Phase 2):** add the `discoveryResults` DB module (CRUD over the `discovery_results` table, migration 074) and wire the opt-in provider-discovery service to persist and read findings through it (`persistDiscoveryResult`, `getDiscoveryResults`, `getDiscoveryResultById`, `markVerified`, `deleteDiscoveryResult`) with `(provider, method, endpoint)` upsert de-duplication. Adds the `/api/discovery/*` HTTP surface — `GET /results`, `GET|DELETE /results/:id`, `POST /scan`, `POST /verify/:id` — under **strict loopback-only** authorization (`/api/discovery/` is in `LOCAL_ONLY_API_PREFIXES` and is NOT manage-scope-bypassable, so the `scan` route's outbound probes can never be reached from a tunnel/remote origin). Adds a **dashboard UI tab** (Tools → Discovery, `/dashboard/discovery`) to run scans and review, verify, or delete findings. The service stays **opt-in / default-off**.
- **feat(api):** expose a read-only provider plugin manifest at `GET /api/v1/provider-plugin-manifest` for sidecar/relay discovery. (thanks @KooshaPari)
- **feat(sidecar):** advertise the provider manifest URL to Bifrost/CLIProxyAPI via the `X-OmniRoute-Provider-Manifest-Url` header (`OMNIROUTE_PROVIDER_MANIFEST_URL`). (thanks @KooshaPari)
### 🔧 Bug Fixes

View File

@@ -1,6 +1,7 @@
{
"_comment": "Catraca de complexidade (check-complexity.mjs, ESLint core rules complexity>=15 e max-lines-per-function>80 sobre src+open-sse+electron+bin via eslint.complexity.config.mjs). Conta total de violacoes; so pode cair. --update ratcheta.",
"count": 2007,
"count": 2015,
"_rebaseline_2026_07_03_6007_sidecar_manifest": "2007->2015. PR #6007 (re-cut) adds sidecar/provider-manifest header wiring and the public manifest URL helper; the feature files themselves introduce 0 new complexity violations (check-complexity flags none in providerPluginManifestUrl.ts). The +8 vs the 2007 release baseline is inherited release/v3.8.44 drift absorbed at merge (check:complexity measures 2015 on the current release tip). Tighten via --update next cycle / at /generate-release Phase 0.",
"_rebaseline_2026_07_02_v3844_ci_observed": "2006->2007 (+1). Local Ubuntu measures 2006 on this tree; the GitHub fast-gates runner measures 2007 (same local-vs-CI off-by-one already documented in _rebaseline_2026_06_26_v3838_release_fast_gate). Use the CI-observed value so the gate is deterministic where it actually runs.",
"_rebaseline_2026_07_02_v3844_post_5939": "2003->2006 (+3). Inherited drift from the release/v3.8.44 merges after 3a3d618fe (#5809 audio translations et al.), surfaced by PR fix#5959: check:complexity measures 2006 on the pristine base (cbd08ef78) WITH AND WITHOUT this PR's one-line CLI change (verified by reverting the file and re-measuring) — the PR is complexity-net-zero. Tighten via --update next cycle.",
"_rebaseline_2026_07_02_v3844_merge_burst": "1995->2003 (+8). Inherited v3.8.44 cycle drift surfaced by PR #5939: check:complexity measures 2003 on BOTH the pristine release tip (3a3d618fe) and this PR's merged HEAD — identical, so all +8 came from the 2026-07-02 merge burst into release/v3.8.44 (#5933 codex schema, #5950 OCR, #5904/#5920 combo, #6000/#6008 executor refactors, etc.) merged while the fast-gates queue was base-red (file-size #5933). PR #5939 itself was verified complexity-net-zero during its own CI cycle (DiscoveryPageClient refactored into hooks/sub-components to stay under max-lines-per-function). Tighten via --update next cycle.",

View File

@@ -265,6 +265,8 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
| `NEXT_PUBLIC_CLOUD_URL` | _(empty)_ | Client-side | Client-side mirror of `CLOUD_URL`. |
| `NEXT_PUBLIC_APP_URL` | _(unset)_ | `src/shared/services/cloudSyncScheduler.ts` | Legacy fallback for `NEXT_PUBLIC_BASE_URL`. |
| `OMNIROUTE_PUBLIC_BASE_URL` | _(unset)_ | Public-origin resolver, image URLs | Highest-priority browser-facing OmniRoute origin used for public URL generation and non-dashboard browser-origin validation (for example `/v1/chatgpt-web/image/<id>`). Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL but the user's browser must fetch images from a LAN, tunnel, or public origin. Do **not** include `/v1`. |
| `OMNIROUTE_PROVIDER_MANIFEST_URL` | _(unset)_ | `open-sse/config/providerPluginManifestUrl.ts` | Absolute provider plugin manifest URL advertised to sidecar clients. When unset, OmniRoute derives `/api/v1/provider-plugin-manifest` from request origin or HOST/PORT. |
| `OMNIROUTE_PUBLIC_PROTOCOL` | `http` | `open-sse/config/providerPluginManifestUrl.ts` | Protocol used when deriving the provider plugin manifest URL from HOST/PORT without a request origin. Set to `https` behind a TLS-terminating public proxy when no explicit `OMNIROUTE_PROVIDER_MANIFEST_URL` is set. |
| `OMNIROUTE_TRUST_PROXY` | _(unset)_ | `src/server/origin/publicOrigin.ts` | Optional trust mode for forwarded public-origin headers. Unset = do not trust `Forwarded` / `X-Forwarded-*` for security decisions. `true` / `loopback` trusts forwarded host/proto only from a token-stamped loopback proxy. `private` / `lan` also trusts private-LAN proxy peers. Prefer explicit `NEXT_PUBLIC_BASE_URL` in production. |
| `OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS` | `180000` (3 min) | `open-sse/executors/chatgpt-web.ts` | Max wait time for an async chatgpt-web image to land via the celsius WebSocket. Increase during upstream queue-deep windows. |
| `OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB` | `256` | `open-sse/services/chatgptImageCache.ts` | Total in-memory byte budget (MB) for the chatgpt-web image cache serving `/v1/chatgpt-web/image/<id>`. Lower on memory-constrained hosts; raise if image generation is heavy and clients race the 30-minute TTL. |

View File

@@ -16,6 +16,11 @@ executor code, OAuth defaults, headers, or process environment state.
The same manifest is available over HTTP at
`GET /api/v1/provider-plugin-manifest` for sidecars that run out-of-process.
OmniRoute advertises that URL to Bifrost and CLIProxyAPI via the
`X-OmniRoute-Provider-Manifest-Url` request header. Set
`OMNIROUTE_PROVIDER_MANIFEST_URL` when the sidecar needs a public or container
network URL instead of the local request origin.
## Goal
Move provider metadata toward a plugin contract so the hot request path can

View File

@@ -0,0 +1,26 @@
export const PROVIDER_PLUGIN_MANIFEST_HEADER = "X-OmniRoute-Provider-Manifest-Url";
export const PROVIDER_PLUGIN_MANIFEST_PATH = "/api/v1/provider-plugin-manifest";
function trimTrailingSlash(value: string): string {
return value.replace(/\/$/, "");
}
export function resolveProviderPluginManifestUrl(origin?: string | null): string {
const configured = process.env.OMNIROUTE_PROVIDER_MANIFEST_URL?.trim();
if (configured) return configured;
if (origin) {
return `${trimTrailingSlash(origin)}${PROVIDER_PLUGIN_MANIFEST_PATH}`;
}
const host = process.env.HOST || "127.0.0.1";
const port = process.env.PORT || process.env.DASHBOARD_PORT || process.env.API_PORT || "20128";
const protocol = process.env.OMNIROUTE_PUBLIC_PROTOCOL || "http";
return `${protocol}://${host}:${port}${PROVIDER_PLUGIN_MANIFEST_PATH}`;
}
export function getProviderPluginManifestHeader(origin?: string | null): Record<string, string> {
return {
[PROVIDER_PLUGIN_MANIFEST_HEADER]: resolveProviderPluginManifestUrl(origin),
};
}

View File

@@ -22,6 +22,7 @@ import {
type ProviderCredentials,
} from "./base.ts";
import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { getProviderPluginManifestHeader } from "../config/providerPluginManifestUrl.ts";
import { cloakThirdPartyToolNames } from "../services/claudeCodeToolRemapper.ts";
import { sanitizeClaudeToolSchemas } from "../translator/helpers/schemaCoercion.ts";
@@ -265,6 +266,7 @@ export class CliproxyapiExecutor extends BaseExecutor {
const headers: Record<string, string> = {
"Content-Type": "application/json",
...getProviderPluginManifestHeader(),
};
if (key) {

View File

@@ -32,6 +32,7 @@ import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
import { createInjectionGuard } from "@/middleware/promptInjectionGuard";
import { getRelayTokenByHash, checkRateLimit, recordRelayUsage } from "@/lib/db/relayProxies";
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
import { getProviderPluginManifestHeader } from "@omniroute/open-sse/config/providerPluginManifestUrl.ts";
import { z } from "zod";
import {
checkIpRateLimit,
@@ -257,6 +258,7 @@ export async function POST(request: Request) {
"Content-Type": "application/json",
"x-relay-token-id": token.id,
"x-relay-client-ip": clientIp,
...getProviderPluginManifestHeader(new URL(request.url).origin),
};
if (BIFROST_API_KEY) {
upstreamHeaders["Authorization"] = `Bearer ${BIFROST_API_KEY}`;

View File

@@ -26,6 +26,7 @@ import {
type BifrostRoutingConfig,
} from "./routingBackend";
import { getProviderPluginManifestEntryForModel } from "@omniroute/open-sse/config/providerPluginManifestRegistry.ts";
import { getProviderPluginManifestHeader } from "@omniroute/open-sse/config/providerPluginManifestUrl.ts";
import { finalizeReadableStream } from "./streamFinalizer";
import {
clearBifrostFailure,
@@ -74,6 +75,7 @@ async function forwardToBifrost(
"Content-Type": "application/json",
"x-relay-token-id": token.id,
"x-relay-client-ip": clientIp,
...getProviderPluginManifestHeader(new URL(request.url).origin),
};
if (config.apiKey) {
upstreamHeaders.Authorization = `Bearer ${config.apiKey}`;

View File

@@ -0,0 +1,39 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
PROVIDER_PLUGIN_MANIFEST_HEADER,
resolveProviderPluginManifestUrl,
getProviderPluginManifestHeader,
} from "../../open-sse/config/providerPluginManifestUrl.ts";
test("provider manifest URL uses explicit env override", () => {
const previous = process.env.OMNIROUTE_PROVIDER_MANIFEST_URL;
process.env.OMNIROUTE_PROVIDER_MANIFEST_URL = "http://sidecar.local/manifest.json";
try {
assert.equal(
resolveProviderPluginManifestUrl("http://127.0.0.1:20128"),
"http://sidecar.local/manifest.json",
);
} finally {
if (previous === undefined) {
delete process.env.OMNIROUTE_PROVIDER_MANIFEST_URL;
} else {
process.env.OMNIROUTE_PROVIDER_MANIFEST_URL = previous;
}
}
});
test("provider manifest URL derives from request origin", () => {
assert.equal(
resolveProviderPluginManifestUrl("http://127.0.0.1:20128/"),
"http://127.0.0.1:20128/api/v1/provider-plugin-manifest",
);
});
test("provider manifest header exposes stable header name", () => {
assert.deepEqual(getProviderPluginManifestHeader("http://localhost:20128"), {
[PROVIDER_PLUGIN_MANIFEST_HEADER]:
"http://localhost:20128/api/v1/provider-plugin-manifest",
});
});