mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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").
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
26
open-sse/config/providerPluginManifestUrl.ts
Normal file
26
open-sse/config/providerPluginManifestUrl.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
39
tests/unit/provider-plugin-manifest-url.test.ts
Normal file
39
tests/unit/provider-plugin-manifest-url.test.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user