mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 22:32:22 +03:00
chore: sync release/v3.8.51 into the branch
This commit is contained in:
@@ -13,6 +13,47 @@ import {
|
||||
} from "@/shared/hooks/useTimestampTitles";
|
||||
import { JsonTreeExpandControls } from "@/shared/components/JsonTreeExpandControls";
|
||||
import { useJsonTreeExpandLevel } from "@/store/jsonTreeExpandStore";
|
||||
import {
|
||||
isPipelineSizeLimitMarker,
|
||||
isSizeLimitOmissionMarker,
|
||||
} from "@/shared/constants/callLogSizeLimitMarkers";
|
||||
|
||||
// ─── Size-limit omission detection (#13894) ─────────────────────────────────
|
||||
// A size-limited call-log artifact does not simply drop a payload -- it writes
|
||||
// an explicit marker in its place (see callLogArtifacts.ts's
|
||||
// omitOversizedPipeline()/buildMinimalArtifactForSizeLimit()). Before this fix
|
||||
// the detail view fed that marker straight into the generic JSON/`<pre>`
|
||||
// renderer, so a size-limit omission was indistinguishable from a real
|
||||
// upstream error or a genuinely empty payload -- a silent fallback. These
|
||||
// helpers turn the marker into an explicit, labeled notice instead.
|
||||
|
||||
/** Builds the pipeline payload sections, replacing the `error` marker object
|
||||
* left by a size-limited pipeline capture with an explicit notice entry
|
||||
* instead of letting it render as if it were a real pipeline error. */
|
||||
export function buildPipelinePayloadSections(entries, pipelinePayloads) {
|
||||
return entries
|
||||
.map(([key, title]) => {
|
||||
const value = pipelinePayloads?.[key];
|
||||
if (key === "error" && isPipelineSizeLimitMarker(value)) {
|
||||
return { key, title, json: null, notice: true };
|
||||
}
|
||||
if (value === null || value === undefined) return { key, title, json: null, notice: false };
|
||||
let json;
|
||||
try {
|
||||
json = JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
json = String(value);
|
||||
}
|
||||
return { key, title, json, notice: false };
|
||||
})
|
||||
.filter((section) => section.json || section.notice);
|
||||
}
|
||||
|
||||
/** True when a top-level requestBody/responseBody was replaced by the
|
||||
* size-limit omission placeholder string rather than genuinely absent. */
|
||||
export function isBodySizeLimitOmission(value) {
|
||||
return isSizeLimitOmissionMarker(value);
|
||||
}
|
||||
|
||||
// ─── Payload Code Block ─────────────────────────────────────────────────────
|
||||
// Renders parsed payloads as a collapsible JSON tree (react18-json-view) so
|
||||
@@ -21,11 +62,15 @@ import { useJsonTreeExpandLevel } from "@/store/jsonTreeExpandStore";
|
||||
// the plain <pre> dump for anything that isn't valid JSON (e.g. a captured
|
||||
// error string), since json is display text sourced from JSON.stringify with
|
||||
// a String() fallback on failure -- it is not guaranteed parseable.
|
||||
// `notice`, when true, takes over rendering entirely: it means `json` is not a
|
||||
// real payload but a size-limit omission marker (#13894) that must be shown as
|
||||
// an explicit, labeled notice rather than a generic JSON/error dump.
|
||||
|
||||
export function PayloadSection({
|
||||
title,
|
||||
sectionId,
|
||||
json,
|
||||
notice = false,
|
||||
onCopy,
|
||||
collapsible = true,
|
||||
defaultOpen = true,
|
||||
@@ -78,20 +123,28 @@ export function PayloadSection({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-text-muted hover:text-text-primary transition-colors"
|
||||
aria-label={t("copyTitle", { title })}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied ? "check" : "content_copy"}
|
||||
</span>
|
||||
{copied ? t("copied") : t("copy")}
|
||||
</button>
|
||||
{!notice && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-text-muted hover:text-text-primary transition-colors"
|
||||
aria-label={t("copyTitle", { title })}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied ? "check" : "content_copy"}
|
||||
</span>
|
||||
{copied ? t("copied") : t("copy")}
|
||||
</button>
|
||||
)}
|
||||
{parsedJson !== null && <JsonTreeExpandControls sectionId={resolvedSectionId} />}
|
||||
</div>
|
||||
</div>
|
||||
{open && parsedJson !== null && (
|
||||
{open && notice && (
|
||||
<div className="p-4 rounded-xl border border-amber-500/40 bg-amber-500/10 text-xs text-amber-700 dark:text-amber-300 flex items-start gap-2">
|
||||
<span className="material-symbols-outlined text-[16px] shrink-0">warning</span>
|
||||
<span>{t("payloadSizeLimitOmitted")}</span>
|
||||
</div>
|
||||
)}
|
||||
{open && !notice && parsedJson !== null && (
|
||||
<div
|
||||
ref={treeContainerRef}
|
||||
className="rounded-xl bg-black/5 dark:bg-black/30 border border-border max-h-150 overflow-auto p-4 text-xs font-mono"
|
||||
@@ -105,7 +158,7 @@ export function PayloadSection({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{open && parsedJson === null && (
|
||||
{open && !notice && parsedJson === null && (
|
||||
<pre className="p-4 rounded-xl bg-black/5 dark:bg-black/30 border border-border overflow-x-auto text-xs font-mono text-text-main max-h-150 overflow-y-auto leading-relaxed whitespace-pre-wrap break-words">
|
||||
{json}
|
||||
</pre>
|
||||
|
||||
@@ -20,6 +20,8 @@ import { useJsonTreeExpandLevel } from "@/store/jsonTreeExpandStore";
|
||||
import {
|
||||
PayloadSection,
|
||||
ConversationContextSection,
|
||||
buildPipelinePayloadSections,
|
||||
isBodySizeLimitOmission,
|
||||
} from "@/shared/components/RequestLoggerDetail.sections";
|
||||
|
||||
// ─── Copy-all composition ────────────────────────────────────────────────────
|
||||
@@ -470,22 +472,21 @@ export default function RequestLoggerDetail({
|
||||
|
||||
const pipelinePayloads = detail?.pipelinePayloads || null;
|
||||
const payloadSections = pipelinePayloads
|
||||
? [
|
||||
["clientRawRequest", t("payload.clientRawRequest")],
|
||||
["clientRequest", t("payload.clientRequest")],
|
||||
["openaiRequest", t("payload.openaiRequest")],
|
||||
["providerRequest", t("payload.providerRequest")],
|
||||
["providerResponse", t("payload.providerResponse")],
|
||||
["clientResponse", t("payload.clientResponse")],
|
||||
["error", t("payload.pipelineError")],
|
||||
]
|
||||
.map(([key, title]) => ({
|
||||
key,
|
||||
title,
|
||||
json: toPrettyJson(pipelinePayloads[key]),
|
||||
}))
|
||||
.filter((section) => section.json)
|
||||
? buildPipelinePayloadSections(
|
||||
[
|
||||
["clientRawRequest", t("payload.clientRawRequest")],
|
||||
["clientRequest", t("payload.clientRequest")],
|
||||
["openaiRequest", t("payload.openaiRequest")],
|
||||
["providerRequest", t("payload.providerRequest")],
|
||||
["providerResponse", t("payload.providerResponse")],
|
||||
["clientResponse", t("payload.clientResponse")],
|
||||
["error", t("payload.pipelineError")],
|
||||
],
|
||||
pipelinePayloads
|
||||
)
|
||||
: [];
|
||||
const requestBodyOmitted = isBodySizeLimitOmission(detail?.requestBody);
|
||||
const responseBodyOmitted = isBodySizeLimitOmission(detail?.responseBody);
|
||||
const requestJson = detail?.requestBody ? toPrettyJson(detail.requestBody) : null;
|
||||
const responseJson = detail?.responseBody ? toPrettyJson(detail.responseBody) : null;
|
||||
const streamChunks = (() => {
|
||||
@@ -1155,6 +1156,7 @@ export default function RequestLoggerDetail({
|
||||
title={section.title}
|
||||
sectionId={section.key}
|
||||
json={section.json}
|
||||
notice={section.notice}
|
||||
onCopy={() => onCopy(section.json)}
|
||||
/>
|
||||
))}
|
||||
@@ -1164,6 +1166,7 @@ export default function RequestLoggerDetail({
|
||||
title={t("responsePayloadLegacy")}
|
||||
sectionId="responsePayloadLegacy"
|
||||
json={responseJson}
|
||||
notice={responseBodyOmitted}
|
||||
onCopy={() => onCopy(responseJson)}
|
||||
/>
|
||||
)}
|
||||
@@ -1173,6 +1176,7 @@ export default function RequestLoggerDetail({
|
||||
title={t("requestPayloadLegacy")}
|
||||
sectionId="requestPayloadLegacy"
|
||||
json={requestJson}
|
||||
notice={requestBodyOmitted}
|
||||
onCopy={() => onCopy(requestJson)}
|
||||
/>
|
||||
)}
|
||||
|
||||
40
src/shared/constants/callLogSizeLimitMarkers.ts
Normal file
40
src/shared/constants/callLogSizeLimitMarkers.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Sentinel markers written by src/lib/usage/callLogArtifacts.ts when a call-log
|
||||
// artifact's request/response body or pipeline payload had to be dropped because
|
||||
// it exceeded the configured size cap (CALL_LOG_PIPELINE_MAX_SIZE_KB /
|
||||
// MAX_CALL_LOG_ARTIFACT_BYTES). Kept here — not inside callLogArtifacts.ts, which
|
||||
// pulls in `fs`/`path` and cannot be imported by a client component — so the
|
||||
// artifact writer and the request-log detail view (RequestLoggerDetail.tsx) share
|
||||
// one definition of "this is a size-limit omission" instead of each guessing at
|
||||
// the shape independently (see issue #13894: the previous frontend rendered the
|
||||
// pipeline marker verbatim as if it were a real upstream error).
|
||||
|
||||
export const CALL_LOG_SIZE_LIMIT_REASON = "call_log_artifact_size_limit_exceeded";
|
||||
|
||||
export const CALL_LOG_BODY_OMITTED_FOR_SIZE_LIMIT =
|
||||
"[omitted: call log artifact size limit exceeded]";
|
||||
|
||||
export const CALL_LOG_STREAM_CHUNKS_OMITTED_FOR_SIZE_LIMIT =
|
||||
"[stream chunks omitted: call log artifact size limit exceeded]";
|
||||
|
||||
/**
|
||||
* True for a placeholder a size-limit fallback wrote in place of a real
|
||||
* requestBody/responseBody/stream-chunk payload.
|
||||
*/
|
||||
export function isSizeLimitOmissionMarker(value: unknown): boolean {
|
||||
return (
|
||||
value === CALL_LOG_BODY_OMITTED_FOR_SIZE_LIMIT ||
|
||||
value === CALL_LOG_STREAM_CHUNKS_OMITTED_FOR_SIZE_LIMIT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* True for the `pipeline.error` marker object omitOversizedPipeline() writes in
|
||||
* place of the real pipeline payload once it exceeds CALL_LOG_PIPELINE_MAX_SIZE_KB.
|
||||
* Checked by shape (not just truthiness) so a real upstream error that happens to
|
||||
* be named `error` is never mistaken for the size-limit marker.
|
||||
*/
|
||||
export function isPipelineSizeLimitMarker(pipelineError: unknown): boolean {
|
||||
if (!pipelineError || typeof pipelineError !== "object") return false;
|
||||
const candidate = pipelineError as { _omniroute_truncated?: unknown; reason?: unknown };
|
||||
return candidate._omniroute_truncated === true && candidate.reason === CALL_LOG_SIZE_LIMIT_REASON;
|
||||
}
|
||||
@@ -197,54 +197,6 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
// Output limit published at https://ai.google.dev/gemini-api/docs/models/gemini-3.8-flash.
|
||||
// Thinking budgets follow the 3.7 Flash high/medium/low/tiered split.
|
||||
"gemini-3.8-flash-high": {
|
||||
maxOutputTokens: 65536,
|
||||
contextWindow: 1048576,
|
||||
defaultThinkingBudget: 24576,
|
||||
thinkingBudgetCap: 24576,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
"gemini-3.8-flash-medium": {
|
||||
maxOutputTokens: 65536,
|
||||
contextWindow: 1048576,
|
||||
defaultThinkingBudget: 8192,
|
||||
thinkingBudgetCap: 24576,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
"gemini-3.8-flash-low": {
|
||||
maxOutputTokens: 65536,
|
||||
contextWindow: 1048576,
|
||||
defaultThinkingBudget: 1024,
|
||||
thinkingBudgetCap: 24576,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
"gemini-3.8-flash": {
|
||||
maxOutputTokens: 65536,
|
||||
contextWindow: 1048576,
|
||||
defaultThinkingBudget: 8192,
|
||||
thinkingBudgetCap: 24576,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
aliases: ["gemini-3.8-flash-tiered"],
|
||||
},
|
||||
"gemini-3.8-flash-tiered": {
|
||||
maxOutputTokens: 65536,
|
||||
contextWindow: 1048576,
|
||||
defaultThinkingBudget: 8192,
|
||||
thinkingBudgetCap: 24576,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
|
||||
// Gemini 3.7 Flash tiers: high 24.5k, medium 8k, low 1k thinking tokens.
|
||||
"gemini-3.7-flash-high": {
|
||||
@@ -293,6 +245,53 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
// ── Gemini 3.8 Flash (current Antigravity/AGY live tiers) ─────────
|
||||
"gemini-3.8-flash-high": {
|
||||
maxOutputTokens: 65536,
|
||||
contextWindow: 1048576,
|
||||
defaultThinkingBudget: 24576,
|
||||
thinkingBudgetCap: 24576,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
"gemini-3.8-flash-medium": {
|
||||
maxOutputTokens: 65536,
|
||||
contextWindow: 1048576,
|
||||
defaultThinkingBudget: 8192,
|
||||
thinkingBudgetCap: 24576,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
"gemini-3.8-flash-low": {
|
||||
maxOutputTokens: 65536,
|
||||
contextWindow: 1048576,
|
||||
defaultThinkingBudget: 1024,
|
||||
thinkingBudgetCap: 24576,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
"gemini-3.8-flash": {
|
||||
maxOutputTokens: 65536,
|
||||
contextWindow: 1048576,
|
||||
defaultThinkingBudget: 8192,
|
||||
thinkingBudgetCap: 24576,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
aliases: ["gemini-3.8-flash-tiered"],
|
||||
},
|
||||
"gemini-3.8-flash-tiered": {
|
||||
maxOutputTokens: 65536,
|
||||
contextWindow: 1048576,
|
||||
defaultThinkingBudget: 8192,
|
||||
thinkingBudgetCap: 24576,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
|
||||
// Provider-neutral compatibility for providers that still serve Gemini 3.6.
|
||||
// Antigravity/AGY availability is governed by their own provider catalogs and
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
* User-supplied upstream extra headers: names we never forward (Host / hop-by-hop / framing).
|
||||
* Changing this list requires syncing: `sanitizeUpstreamHeadersMap` (models.ts), Zod
|
||||
* `upstreamHeaderNameSchema` / record refine (schemas.ts), and `upstream-headers-sanitize` tests.
|
||||
*
|
||||
* The forwarding/IP set (x-forwarded-for, x-real-ip, cf-connecting-ip, forwarded, via, …)
|
||||
* is forbidden so the client-origin IP can never be disclosed (or spoofed) to the upstream
|
||||
* provider through an operator-set custom upstream header. This mirrors the established
|
||||
* scrubbers/denylists already used by the Antigravity (`antigravityHeaderScrub.ts`) and
|
||||
* Cursor CLI (`cursorCliProxy.ts`) paths, extended here to cover every provider.
|
||||
*/
|
||||
const FORBIDDEN = new Set(
|
||||
[
|
||||
@@ -24,6 +30,18 @@ const FORBIDDEN = new Set(
|
||||
"te",
|
||||
"trailer",
|
||||
"upgrade",
|
||||
// Origin-IP disclosure: never send the client's forwarding headers upstream.
|
||||
"x-forwarded-for",
|
||||
"x-forwarded-host",
|
||||
"x-forwarded-proto",
|
||||
"x-forwarded-port",
|
||||
"x-forwarded-server",
|
||||
"x-real-ip",
|
||||
"cf-connecting-ip",
|
||||
"true-client-ip",
|
||||
"client-ip",
|
||||
"forwarded",
|
||||
"via",
|
||||
].map((s) => s.toLowerCase())
|
||||
);
|
||||
|
||||
|
||||
@@ -209,7 +209,11 @@ export function resolveDisplayBaseUrl(
|
||||
return joinOriginAndBasePath(configuredUrl, basePath);
|
||||
}
|
||||
|
||||
const fallback = currentOrigin ?? configuredUrl ?? DEFAULT_DISPLAY_BASE_URL;
|
||||
const portFallback =
|
||||
typeof process !== "undefined" && (process.env.NEXT_PUBLIC_PORT || process.env.PORT)
|
||||
? `http://localhost:${process.env.NEXT_PUBLIC_PORT || process.env.PORT}`
|
||||
: DEFAULT_DISPLAY_BASE_URL;
|
||||
const fallback = currentOrigin ?? configuredUrl ?? portFallback;
|
||||
return joinOriginAndBasePath(fallback, basePath);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
type IngestBudgetAcquireResult,
|
||||
} from "./ingestByteAdmission";
|
||||
import {
|
||||
checkResourcePressureGuard,
|
||||
getResourcePressureObservation,
|
||||
type PressureSeverity,
|
||||
} from "@omniroute/open-sse/utils/resourcePressure.ts";
|
||||
@@ -217,10 +218,45 @@ export type ChatAdmissionShedReason =
|
||||
| "inflight_bytes_budget"
|
||||
| "resource_pressure";
|
||||
|
||||
/** Read cached pressure severity; sampling failures must not cause false sheds. */
|
||||
/**
|
||||
* Read pressure severity for admission decisions.
|
||||
*
|
||||
* This MUST drive an active re-sample (`checkResourcePressureGuard`), not a
|
||||
* passive cache read of `getResourcePressureObservation`. The resource-pressure
|
||||
* runtime only refreshes its sample and re-evaluates recovery from *inside*
|
||||
* `check()` (via `scheduleRefresh`) — nothing else in the singleton mutates
|
||||
* `state` or schedules a refresh. The structural admission gate that calls
|
||||
* this function runs *before* every other code path that would otherwise call
|
||||
* `check()` (`handleChatCore`, `checkResourcePressureBeforeProviderWork`,
|
||||
* `AdaptiveAdmissionRuntimeImpl.acquire`) — so once `state.severity` flips to
|
||||
* "critical", a passive read here sheds every subsequent request before any
|
||||
* of those downstream paths can run, which means `check()` never gets called
|
||||
* again and the guard can never observe recovery. See
|
||||
* https://github.com/diegosouzapw/OmniRoute/issues/13821.
|
||||
*
|
||||
* `checkResourcePressureGuard()` is cheap on the hot path: it only does a
|
||||
* synchronous `process.memoryUsage()` read plus a timestamp comparison per
|
||||
* call; the actual signal sampling (`/proc/pressure/memory`, cgroup reads)
|
||||
* happens asynchronously via `scheduleRefresh()` and is throttled by
|
||||
* `staleAfterMs`, so calling this on every admitted request does not add
|
||||
* per-request I/O.
|
||||
*
|
||||
* A non-null guard is this request's authoritative "shed now" answer and maps
|
||||
* to "critical". A null guard means this request is not shed, but the
|
||||
* observation's cached label can still read "critical" for a few more
|
||||
* milliseconds until the async refresh settles (or if the last real sample
|
||||
* merely went stale — `check()`'s own `maxStaleMs` fallback) — reporting that
|
||||
* stale "critical" label to callers that branch on severity (e.g. the queue
|
||||
* wait sizing at admitChatRequest's `reserve()`) would just re-introduce the
|
||||
* same "never downgrades" problem for the "high" queueing bucket, so it is
|
||||
* downgraded to "high" here instead.
|
||||
*/
|
||||
export function defaultPressureSeverity(): PressureSeverity {
|
||||
try {
|
||||
return getResourcePressureObservation().state.severity;
|
||||
const guard = checkResourcePressureGuard();
|
||||
if (guard) return "critical";
|
||||
const severity = getResourcePressureObservation().state.severity;
|
||||
return severity === "critical" ? "high" : severity;
|
||||
} catch {
|
||||
return "normal";
|
||||
}
|
||||
|
||||
@@ -146,6 +146,16 @@ async function readResponseBuffer(response: Response, maxBytes: number) {
|
||||
return Buffer.concat(chunks, totalBytes);
|
||||
}
|
||||
|
||||
// #13883: test-only escape hatch for `pinDns: true` callers that have no `fetchImpl` seam
|
||||
// of their own (imageGeneration.ts / imageUpscale/shared.ts). `createPinnedFetch` opens a
|
||||
// real undici connection, bypassing a test's monkeypatched `globalThis.fetch`; setting this
|
||||
// override lets such a test keep exercising its mock instead of a real network attempt.
|
||||
// Production callers never call the setter, so `pinDns` still pins for real in production.
|
||||
let pinnedFetchTestOverride: typeof fetch | undefined;
|
||||
export function setPinnedFetchTestOverride(fetchImpl: typeof fetch | undefined): void {
|
||||
pinnedFetchTestOverride = fetchImpl;
|
||||
}
|
||||
|
||||
export async function fetchRemoteMedia(
|
||||
input: string | URL,
|
||||
options: RemoteMediaFetchOptions = {}
|
||||
@@ -171,6 +181,7 @@ export async function fetchRemoteMedia(
|
||||
const addresses = await assertHostnameResolvesPublic(currentUrl, guard, lookup);
|
||||
const fetchImpl =
|
||||
injectedFetch ??
|
||||
pinnedFetchTestOverride ??
|
||||
(pinDns && addresses.length
|
||||
? createPinnedFetch(addresses[0].address, addresses[0].family)
|
||||
: fetch);
|
||||
|
||||
@@ -75,6 +75,7 @@ export interface ApiKeyMetadata {
|
||||
name?: string;
|
||||
modelAccessMode?: "all" | "restricted";
|
||||
allowedModels?: string[];
|
||||
blockedModels?: string[];
|
||||
allowedCombos?: string[];
|
||||
allowedConnections?: string[];
|
||||
allowedQuotas?: string[];
|
||||
@@ -346,6 +347,7 @@ async function validateStandardRoutingTarget(
|
||||
const hasModelRestrictions =
|
||||
apiKeyInfo.modelAccessMode === "restricted" ||
|
||||
Boolean(apiKeyInfo.allowedModels?.length) ||
|
||||
Boolean(apiKeyInfo.blockedModels?.length) ||
|
||||
apiKeyInfo.disableNonPublicModels === true;
|
||||
if (!requestedComboName && hasModelRestrictions && modelStr.startsWith("auto/")) {
|
||||
requestedComboName = modelStr;
|
||||
@@ -587,6 +589,7 @@ async function validateModelAccess(context: PolicyContext): Promise<Response | n
|
||||
const hasModelRestrictions =
|
||||
apiKeyInfo.modelAccessMode === "restricted" ||
|
||||
Boolean(apiKeyInfo.allowedModels?.length) ||
|
||||
Boolean(apiKeyInfo.blockedModels?.length) ||
|
||||
apiKeyInfo.disableNonPublicModels === true;
|
||||
if (!requestedComboName && hasModelRestrictions) {
|
||||
if (modelStr.startsWith("auto/") || modelStr.startsWith("qtSd/")) {
|
||||
|
||||
@@ -103,6 +103,12 @@ const QUOTA_PATTERNS: ReadonlyArray<RegExp> = [
|
||||
/\bTPD rate limit\b/i,
|
||||
/insufficient balance/i,
|
||||
|
||||
// xAI Grok Build free-tier per-model rolling 24h cap. Live body:
|
||||
// "You've used all the included free usage for model grok-4.6 for now.
|
||||
// Usage resets over a rolling 24-hour window — tokens (actual/limit): N/M."
|
||||
/used all the included free usage/i,
|
||||
/resets over a rolling 24-hour window/i,
|
||||
|
||||
// ── CJK quota-exhaustion patterns (#13194) ────────────────────────────
|
||||
// Chinese (simplified) providers (z.ai/GLM, Kimi/Moonshot, Qwen/DashScope,
|
||||
// MiniMax) return 429 bodies entirely in Chinese. Without these, the
|
||||
|
||||
@@ -4,6 +4,9 @@ type OmniRouteBaseUrlEnv = {
|
||||
OMNIROUTE_BASE_URL?: string;
|
||||
BASE_URL?: string;
|
||||
NEXT_PUBLIC_BASE_URL?: string;
|
||||
PORT?: string | number;
|
||||
API_PORT?: string | number;
|
||||
DASHBOARD_PORT?: string | number;
|
||||
};
|
||||
|
||||
function normalizeBaseUrl(value?: string): string | null {
|
||||
@@ -13,11 +16,14 @@ function normalizeBaseUrl(value?: string): string | null {
|
||||
}
|
||||
|
||||
export function resolveOmniRouteBaseUrl(env: OmniRouteBaseUrlEnv = process.env): string {
|
||||
const port = env.PORT || env.API_PORT || env.DASHBOARD_PORT;
|
||||
const fallback = port ? `http://localhost:${port}` : DEFAULT_OMNIROUTE_BASE_URL;
|
||||
|
||||
return (
|
||||
normalizeBaseUrl(env.OMNIROUTE_BASE_URL) ||
|
||||
normalizeBaseUrl(env.BASE_URL) ||
|
||||
normalizeBaseUrl(env.NEXT_PUBLIC_BASE_URL) ||
|
||||
DEFAULT_OMNIROUTE_BASE_URL
|
||||
fallback
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -432,8 +432,9 @@ export const updateComboSchema = z
|
||||
// so the one endpoint a client can flip it through stripped the field and
|
||||
// a visibility-only update was rejected as empty. #12836
|
||||
isHidden: z.boolean().optional(),
|
||||
allowedProviders: z.array(z.string().trim().min(1).max(200)).max(100).optional(),
|
||||
allowedModelFamilies: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
|
||||
allowedProviders: z.array(z.string().trim().min(1).max(200)).max(100).optional().nullable(),
|
||||
allowedModelFamilies: z.array(z.string().trim().min(1).max(100)).max(100).optional().nullable(),
|
||||
overrideAllowedProviders: z.boolean().optional(),
|
||||
// Nullable like `description` and `context_length` above: an absent field means
|
||||
// "leave unchanged" because updateCombo merges over the stored record, so clearing
|
||||
// one needs an explicit null for updateCombo's null-means-delete pass (#12158).
|
||||
|
||||
@@ -61,6 +61,7 @@ export const createKeySchema = z
|
||||
dailyUsageLimitUsd: z.coerce.number().min(0).optional().nullable(),
|
||||
weeklyUsageLimitUsd: z.coerce.number().min(0).optional().nullable(),
|
||||
chaosModeEnabled: z.boolean().optional(),
|
||||
expiresAt: z.string().datetime().nullable().optional(),
|
||||
scopes: z.array(z.string().trim().min(1).max(64)).max(32).optional(),
|
||||
allowedConnections: z.array(z.string().uuid()).min(1).max(100).optional(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user