mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
This commit is contained in:
committed by
GitHub
parent
29bb59e18d
commit
6bb3207912
@@ -0,0 +1 @@
|
||||
- feat(api): add a structured `X-Routing-Fallback-Reason` header to relay routing responses, exposing a stable machine-readable reason code alongside the legacy `X-Routing-Fallback` detail string (#6872)
|
||||
@@ -1077,7 +1077,7 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
|
||||
| `BIFROST_STREAMING_ENABLED` | `true` | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | When true, the Bifrost sidecar route streams responses back via SSE through the gateway rather than the TS streaming executor. Set to `0` to force non-streaming JSON responses through the gateway. |
|
||||
| `BIFROST_TIMEOUT_MS` | `30000` | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | Per-request timeout when proxying to the Bifrost gateway (ms). On timeout the route returns the TS relay path via the `X-Bifrost-Fallback` header. |
|
||||
| `OMNIROUTE_BIFROST_KEY` | _(unset)_ | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | Alias for `BIFROST_API_KEY` (used by scripts that read the env via `OMNIROUTE_*`). `BIFROST_API_KEY` takes precedence when both are set. |
|
||||
| `OMNIROUTE_RELAY_BACKEND` | `ts` / `auto` | `src/app/api/v1/relay/chat/completions/routingBackend.ts` | Relay backend for `/api/v1/relay/chat/completions`: `ts \| bifrost \| auto`. `ts` = TypeScript relay (default when Bifrost unconfigured); `auto` selects Bifrost when `BIFROST_BASE_URL` is set and `BIFROST_ENABLED` ≠ `0`, with automatic TS fallback if the sidecar is unreachable; `bifrost` forces Bifrost (strict, no fallback). Auth/rate-limit/injection-guard/allowlist always run in the Next route first. Responses carry `X-Routing-Backend` / `X-Routing-Fallback`. |
|
||||
| `OMNIROUTE_RELAY_BACKEND` | `ts` / `auto` | `src/app/api/v1/relay/chat/completions/routingBackend.ts` | Relay backend for `/api/v1/relay/chat/completions`: `ts \| bifrost \| auto`. `ts` = TypeScript relay (default when Bifrost unconfigured); `auto` selects Bifrost when `BIFROST_BASE_URL` is set and `BIFROST_ENABLED` ≠ `0`, with automatic TS fallback if the sidecar is unreachable; `bifrost` forces Bifrost (strict, no fallback). Auth/rate-limit/injection-guard/allowlist always run in the Next route first. Responses carry `X-Routing-Backend` / `X-Routing-Fallback` / `X-Routing-Fallback-Reason`. |
|
||||
| `RELAY_ROUTING_BACKEND` | _(unset)_ | `src/app/api/v1/relay/chat/completions/routingBackend.ts` | Accepted alias for `OMNIROUTE_RELAY_BACKEND` (same `ts \| bifrost \| auto` values). `OMNIROUTE_RELAY_BACKEND` takes precedence when both are set. |
|
||||
| `OMNIROUTE_BIFROST_FAILURE_COOLDOWN_MS` | `5000` | `src/app/api/v1/relay/chat/completions/bifrostCooldown.ts` | Cooldown (ms) after a Bifrost sidecar hop fails in `auto` mode before the relay re-attempts the sidecar; it routes straight to the TS path while the cooldown lasts, then probes again. `0` disables. Only applies when `OMNIROUTE_RELAY_BACKEND=auto`. |
|
||||
| `OMNIROUTE_TLS_CERT` | _(unset)_ | `bin/cli/commands/serve.mjs` | Path to a PEM TLS certificate to serve `omniroute serve` over HTTPS (equivalent to `--tls-cert`). Must be paired with `OMNIROUTE_TLS_KEY`; the standalone server then terminates TLS on the same listener (`wss://` works unchanged). Unset → plain HTTP. Providing only one of cert/key, or an unreadable path, logs a warning and stays HTTP. |
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import {
|
||||
getBifrostRoutingConfig,
|
||||
getRoutingFallbackHeader,
|
||||
getRoutingFallbackReasonHeader,
|
||||
resolveRelayRoutingBackend,
|
||||
shouldTryBifrostForRequest,
|
||||
type BifrostRoutingConfig,
|
||||
@@ -378,6 +379,12 @@ export async function POST(request: Request) {
|
||||
// #5526 helper gates emission (auto + enabled); #5519 dynamic cooldown/error
|
||||
// reason wins as the value when set, else falls back to the static "bifrost".
|
||||
newHeaders.set("X-Routing-Fallback", bifrostFallbackReason ?? routingFallback);
|
||||
// #6872: stable, machine-readable companion header — one of the 4 enum
|
||||
// reason codes, or unset when the legacy value has no specific reason.
|
||||
const fallbackReasonCode = getRoutingFallbackReasonHeader(bifrostFallbackReason);
|
||||
if (fallbackReasonCode) {
|
||||
newHeaders.set("X-Routing-Fallback-Reason", fallbackReasonCode);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(response.body, {
|
||||
|
||||
@@ -102,3 +102,33 @@ export function getRoutingFallbackHeader(
|
||||
): "bifrost" | undefined {
|
||||
return backend === "auto" && config?.enabled ? "bifrost" : undefined;
|
||||
}
|
||||
|
||||
export type RoutingFallbackReasonCode =
|
||||
| "bifrost-cooldown"
|
||||
| "bifrost-error"
|
||||
| "bifrost-ineligible"
|
||||
| "bifrost-provider-unknown";
|
||||
|
||||
const ROUTING_FALLBACK_REASON_CODES = new Set<RoutingFallbackReasonCode>([
|
||||
"bifrost-cooldown",
|
||||
"bifrost-error",
|
||||
"bifrost-ineligible",
|
||||
"bifrost-provider-unknown",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Derives the stable, machine-readable reason code for X-Routing-Fallback-Reason
|
||||
* from the existing (possibly parameterized) X-Routing-Fallback detail string.
|
||||
* #6872: splits the enum token from the legacy ad-hoc detail (e.g. strips the
|
||||
* "; remaining=<ms>" suffix on the cooldown case) without changing the legacy
|
||||
* X-Routing-Fallback value itself.
|
||||
*/
|
||||
export function getRoutingFallbackReasonHeader(
|
||||
fallbackReason: string | null | undefined
|
||||
): RoutingFallbackReasonCode | undefined {
|
||||
if (!fallbackReason) return undefined;
|
||||
const code = fallbackReason.split(";", 1)[0]?.trim();
|
||||
return code && ROUTING_FALLBACK_REASON_CODES.has(code as RoutingFallbackReasonCode)
|
||||
? (code as RoutingFallbackReasonCode)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readFileSync } from "node:fs";
|
||||
import {
|
||||
getBifrostRoutingConfig,
|
||||
getRoutingFallbackHeader,
|
||||
getRoutingFallbackReasonHeader,
|
||||
resolveRelayRoutingBackend,
|
||||
shouldTryBifrost,
|
||||
shouldTryBifrostForRequest,
|
||||
@@ -176,3 +177,29 @@ test("automatic relay keeps the Bifrost timeout active until an SSE stream final
|
||||
assert.match(streamBranch, /error && backend === "auto"/);
|
||||
assert.match(streamBranch, /recordBifrostFailure\(/);
|
||||
});
|
||||
|
||||
test("relay routing fallback reason header strips dynamic cooldown detail to the stable code", () => {
|
||||
assert.equal(
|
||||
getRoutingFallbackReasonHeader("bifrost-cooldown; remaining=1500"),
|
||||
"bifrost-cooldown"
|
||||
);
|
||||
});
|
||||
|
||||
test("relay routing fallback reason header passes already-stable reasons through unchanged", () => {
|
||||
assert.equal(getRoutingFallbackReasonHeader("bifrost-error"), "bifrost-error");
|
||||
assert.equal(getRoutingFallbackReasonHeader("bifrost-ineligible"), "bifrost-ineligible");
|
||||
assert.equal(
|
||||
getRoutingFallbackReasonHeader("bifrost-provider-unknown"),
|
||||
"bifrost-provider-unknown"
|
||||
);
|
||||
});
|
||||
|
||||
test("relay routing fallback reason header stays unset for the bare static legacy value", () => {
|
||||
assert.equal(getRoutingFallbackReasonHeader("bifrost"), undefined);
|
||||
});
|
||||
|
||||
test("relay routing fallback reason header stays unset for null/undefined/unrecognized input", () => {
|
||||
assert.equal(getRoutingFallbackReasonHeader(null), undefined);
|
||||
assert.equal(getRoutingFallbackReasonHeader(undefined), undefined);
|
||||
assert.equal(getRoutingFallbackReasonHeader("something-unrecognized"), undefined);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user