mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
feat(dashboard): routing/settings UX clarity — share %, Cloud Sync rename, base-URL override (#6147) (#6253)
feat(dashboard): routing/settings UX clarity (#6147) — effective share %, Cloud Sync→Remote Settings Sync rename, opt-in advanced base-URL override. TDD guard routing-settings-ux-6147.test.ts (6/6). Base-reds only. Integrated into release/v3.8.45.
This commit is contained in:
committed by
GitHub
parent
776a7a3a58
commit
cefbcfb278
@@ -7,6 +7,7 @@
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127)
|
||||
- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18).
|
||||
- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38)
|
||||
- **feat(dashboard):** routing/settings UX clarity ([#6147](https://github.com/diegosouzapw/OmniRoute/issues/6147)) — (1) weighted combos show the **effective routing share %** next to each weight when weights don't sum to 100 (`WeightTotalBar.tsx`); (2) the status widget's user-facing **"Cloud Sync" label is renamed** to "Remote Settings Sync" (`CloudSyncStatus.tsx`; internal ids/state untouched); (3) built-in providers gain an **opt-in advanced base-URL override** (`isBaseUrlOverrideEligibleProvider`, hidden behind an "Advanced" toggle, reusing the existing `providerSpecificData.baseUrl` persistence — not globally widened). Regression guard: `tests/unit/routing-settings-ux-6147.test.ts`.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
@@ -20,6 +20,20 @@ const WEIGHT_COLORS = [
|
||||
"bg-indigo-500",
|
||||
];
|
||||
|
||||
/**
|
||||
* #6147 — effective routing share of a weighted target.
|
||||
*
|
||||
* The per-target `weight` values do not have to sum to 100; at routing time each
|
||||
* target is picked with probability `weight / Σweights`. So a raw weight of 30
|
||||
* with a total of 60 is an *effective* 50% share. This pure helper computes that
|
||||
* share (0-100) and guards the `total === 0` case so the UI never renders NaN.
|
||||
*/
|
||||
export function effectiveSharePercent(weight: number, total: number): number {
|
||||
if (!weight || weight <= 0) return 0;
|
||||
if (!total || total <= 0) return 0;
|
||||
return (weight / total) * 100;
|
||||
}
|
||||
|
||||
export default function WeightTotalBar({ models }: WeightTotalBarProps) {
|
||||
const total = models.reduce((sum, m) => sum + (m.weight || 0), 0);
|
||||
const isValid = total === 100;
|
||||
@@ -49,6 +63,16 @@ export default function WeightTotalBar({ models }: WeightTotalBarProps) {
|
||||
className={`inline-block w-1.5 h-1.5 rounded-full ${WEIGHT_COLORS[i % WEIGHT_COLORS.length]}`}
|
||||
/>
|
||||
{m.weight}%
|
||||
{/* #6147 — show the *effective* routing share when weights don't sum to 100 */}
|
||||
{total > 0 && total !== 100 && (
|
||||
<span
|
||||
className="text-text-muted/70"
|
||||
title="Effective routing share (weight ÷ total)"
|
||||
>
|
||||
{" → "}
|
||||
{Math.round(effectiveSharePercent(m.weight, total))}%
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { isClaudeExtraUsageBlockEnabled } from "@/lib/providers/claudeExtraUsage
|
||||
import { resolveDashboardProviderInfo } from "../../../providerPageUtils";
|
||||
import {
|
||||
isBaseUrlConfigurableProvider,
|
||||
isBaseUrlOverrideEligibleProvider,
|
||||
getProviderBaseUrlDefault,
|
||||
getProviderBaseUrlHint,
|
||||
getProviderBaseUrlPlaceholder,
|
||||
@@ -153,7 +154,19 @@ export default function EditConnectionModal({
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const showEmail = useEmailPrivacyStore((state) => state.emailsVisible);
|
||||
|
||||
const usesBaseUrl = isBaseUrlConfigurableProvider(provider);
|
||||
// #6147 — built-in providers can opt in to an advanced base-URL override.
|
||||
// OAuth connections are excluded: their save path does not persist
|
||||
// providerSpecificData.baseUrl.
|
||||
const isConfigurableBaseUrl = isBaseUrlConfigurableProvider(provider);
|
||||
const isBaseUrlOverrideEligible =
|
||||
connection.authType !== "oauth" && isBaseUrlOverrideEligibleProvider(provider);
|
||||
const [showBaseUrlOverride, setShowBaseUrlOverride] = useState(
|
||||
() =>
|
||||
typeof connection.providerSpecificData?.baseUrl === "string" &&
|
||||
connection.providerSpecificData.baseUrl.trim().length > 0
|
||||
);
|
||||
const usesBaseUrl =
|
||||
isConfigurableBaseUrl || (isBaseUrlOverrideEligible && showBaseUrlOverride);
|
||||
const defaultBaseUrl = getProviderBaseUrlDefault(provider);
|
||||
const isVertex = provider === "vertex" || provider === "vertex-partner";
|
||||
const isBedrock = provider === "bedrock";
|
||||
@@ -441,12 +454,18 @@ export default function EditConnectionModal({
|
||||
|
||||
let validatedBaseUrl = null;
|
||||
if (usesBaseUrl) {
|
||||
const checked = normalizeAndValidateHttpBaseUrl(formData.baseUrl, defaultBaseUrl);
|
||||
if (checked.error) {
|
||||
setSaveError(checked.error);
|
||||
return;
|
||||
// #6147 — an opt-in override left blank clears it (no default to fall
|
||||
// back to). Configurable providers keep their existing default-fallback.
|
||||
if (!isConfigurableBaseUrl && !formData.baseUrl.trim()) {
|
||||
validatedBaseUrl = null;
|
||||
} else {
|
||||
const checked = normalizeAndValidateHttpBaseUrl(formData.baseUrl, defaultBaseUrl);
|
||||
if (checked.error) {
|
||||
setSaveError(checked.error);
|
||||
return;
|
||||
}
|
||||
validatedBaseUrl = checked.value;
|
||||
}
|
||||
validatedBaseUrl = checked.value;
|
||||
}
|
||||
|
||||
if (!isOAuth && formData.apiKey) {
|
||||
@@ -937,13 +956,33 @@ export default function EditConnectionModal({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* #6147 — opt-in "Advanced → override base URL" for eligible built-ins */}
|
||||
{!usesBaseUrl && isBaseUrlOverrideEligible && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowBaseUrlOverride(true)}
|
||||
className="self-start text-xs text-primary hover:underline"
|
||||
>
|
||||
{providerText(t, "overrideBaseUrlAdvanced", "Advanced: override base URL")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{usesBaseUrl && (
|
||||
<Input
|
||||
label={t("baseUrlLabel")}
|
||||
value={formData.baseUrl}
|
||||
onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })}
|
||||
placeholder={getProviderBaseUrlPlaceholder(provider)}
|
||||
hint={getProviderBaseUrlHint(provider, t)}
|
||||
hint={
|
||||
getProviderBaseUrlHint(provider, t) ||
|
||||
(isBaseUrlOverrideEligible
|
||||
? providerText(
|
||||
t,
|
||||
"overrideBaseUrlHint",
|
||||
"Advanced: point this built-in provider at a custom endpoint. Leave blank to use the default."
|
||||
)
|
||||
: undefined)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -257,6 +257,25 @@ export function isBaseUrlConfigurableProvider(providerId?: string | null) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* #6147 — whether a built-in provider is eligible for an OPT-IN "Advanced →
|
||||
* override base URL" affordance in the edit-connection modal.
|
||||
*
|
||||
* This does NOT widen the always-on base-URL field: providers already covered by
|
||||
* `isBaseUrlConfigurableProvider` (the configurable set + self-hosted) keep their
|
||||
* existing dedicated field and return `false` here. Every *other* provider id is
|
||||
* eligible to opt in per-connection so an operator can hot-fix a broken built-in
|
||||
* preset by pointing it at a custom endpoint. The field stays hidden until the
|
||||
* user explicitly reveals it (or an override was already saved), so nothing is
|
||||
* exposed by default. OAuth connections are excluded at the call site, since
|
||||
* their save path does not persist `providerSpecificData.baseUrl`.
|
||||
*/
|
||||
export function isBaseUrlOverrideEligibleProvider(providerId?: string | null): boolean {
|
||||
if (!providerId) return false;
|
||||
if (isBaseUrlConfigurableProvider(providerId)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getProviderBaseUrlDefault(providerId?: string | null) {
|
||||
const localProvider = getLocalProviderMetadata(providerId);
|
||||
if (typeof localProvider?.localDefault === "string" && localProvider.localDefault.trim()) {
|
||||
|
||||
@@ -4849,6 +4849,8 @@
|
||||
"kimiWebDesc": "Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)",
|
||||
"doubaoWebLabel": "Dola Web",
|
||||
"doubaoWebDesc": "ByteDance AI chat via dola.com",
|
||||
"overrideBaseUrlAdvanced": "Advanced: override base URL",
|
||||
"overrideBaseUrlHint": "Advanced: point this built-in provider at a custom endpoint. Leave blank to use the default.",
|
||||
"bulkAddFormatHintCloudflare": "One key per line. Format: name|accountId|apiKey (Cloudflare account ID + API token)."
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -13,11 +13,15 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
// #6147 — user-facing labels renamed from "Cloud …" to "Remote Settings Sync"
|
||||
// wording (this feature syncs the operator's own settings to their own remote
|
||||
// store — it is not a cloud/telemetry service). Internal state keys, the
|
||||
// `cloud_*` material icons and the cloudSync.* wiring are intentionally kept.
|
||||
const STATUS_CONFIG = {
|
||||
connected: { icon: "cloud_done", color: "text-green-500", label: "Cloud" },
|
||||
connected: { icon: "cloud_done", color: "text-green-500", label: "Synced" },
|
||||
syncing: { icon: "cloud_sync", color: "text-blue-400 animate-pulse", label: "Syncing..." },
|
||||
disconnected: { icon: "cloud_off", color: "text-amber-500", label: "Cloud Off" },
|
||||
error: { icon: "cloud_off", color: "text-red-400", label: "Cloud Error" },
|
||||
disconnected: { icon: "cloud_off", color: "text-amber-500", label: "Sync Off" },
|
||||
error: { icon: "cloud_off", color: "text-red-400", label: "Sync Error" },
|
||||
disabled: { icon: "cloud_off", color: "text-text-muted/50", label: "Disabled" },
|
||||
};
|
||||
|
||||
@@ -80,10 +84,10 @@ export default function CloudSyncStatus({ collapsed = false }) {
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-xs rounded-lg hover:bg-white/5 transition-colors cursor-pointer w-full"
|
||||
title={
|
||||
lastSync
|
||||
? `Cloud ${status === "connected" ? "connected" : "disconnected"} — Last sync: ${lastSync.toLocaleTimeString()}`
|
||||
? `Remote settings sync ${status === "connected" ? "connected" : "disconnected"} — Last sync: ${lastSync.toLocaleTimeString()}`
|
||||
: config.label
|
||||
}
|
||||
aria-label={`Cloud sync status: ${config.label}`}
|
||||
aria-label={`Remote settings sync status: ${config.label}`}
|
||||
>
|
||||
<span className={`material-symbols-outlined text-[16px] ${config.color}`} aria-hidden="true">
|
||||
{config.icon}
|
||||
|
||||
53
tests/unit/routing-settings-ux-6147.test.ts
Normal file
53
tests/unit/routing-settings-ux-6147.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// #6147 — routing/settings UX clarity. Pure-function guards for two of the three
|
||||
// sub-fixes:
|
||||
// 1. Weighted effective-share % (weight ÷ Σweights), incl. total===0 and sum≠100.
|
||||
// 3. Opt-in base-URL override eligibility predicate for built-in providers.
|
||||
// (Item 2 is a display-only label rename with no pure seam — manual verification.)
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { effectiveSharePercent } from "../../src/app/(dashboard)/dashboard/combos/WeightTotalBar.tsx";
|
||||
import {
|
||||
isBaseUrlOverrideEligibleProvider,
|
||||
isBaseUrlConfigurableProvider,
|
||||
} from "../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts";
|
||||
|
||||
test("effectiveSharePercent — sum === 100 keeps raw weight", () => {
|
||||
assert.equal(effectiveSharePercent(30, 100), 30);
|
||||
assert.equal(effectiveSharePercent(70, 100), 70);
|
||||
});
|
||||
|
||||
test("effectiveSharePercent — sum !== 100 rescales to the effective share", () => {
|
||||
// Two equal weights of 30 (total 60) are each an effective 50%.
|
||||
assert.equal(effectiveSharePercent(30, 60), 50);
|
||||
// Over-100 total: 150 total, weight 75 → 50%.
|
||||
assert.equal(effectiveSharePercent(75, 150), 50);
|
||||
});
|
||||
|
||||
test("effectiveSharePercent — guards total === 0 and non-positive weight (no NaN)", () => {
|
||||
assert.equal(effectiveSharePercent(0, 0), 0);
|
||||
assert.equal(effectiveSharePercent(50, 0), 0);
|
||||
assert.equal(effectiveSharePercent(0, 100), 0);
|
||||
assert.equal(effectiveSharePercent(-10, 100), 0);
|
||||
assert.ok(!Number.isNaN(effectiveSharePercent(10, 0)));
|
||||
});
|
||||
|
||||
test("isBaseUrlOverrideEligibleProvider — built-in providers are opt-in eligible", () => {
|
||||
// A plain built-in that has no dedicated base-URL field today.
|
||||
assert.equal(isBaseUrlOverrideEligibleProvider("openai"), true);
|
||||
assert.equal(isBaseUrlOverrideEligibleProvider("groq"), true);
|
||||
});
|
||||
|
||||
test("isBaseUrlOverrideEligibleProvider — providers with an always-on field are NOT re-offered", () => {
|
||||
// Everything already covered by the configurable set stays false here so the
|
||||
// override does not double up on the existing dedicated field.
|
||||
for (const id of ["azure-openai", "databricks", "siliconflow"]) {
|
||||
assert.equal(isBaseUrlConfigurableProvider(id), true);
|
||||
assert.equal(isBaseUrlOverrideEligibleProvider(id), false);
|
||||
}
|
||||
});
|
||||
|
||||
test("isBaseUrlOverrideEligibleProvider — empty/nullish ids are not eligible", () => {
|
||||
assert.equal(isBaseUrlOverrideEligibleProvider(""), false);
|
||||
assert.equal(isBaseUrlOverrideEligibleProvider(null), false);
|
||||
assert.equal(isBaseUrlOverrideEligibleProvider(undefined), false);
|
||||
});
|
||||
Reference in New Issue
Block a user