Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c40ef0964 | |||
| ceb778a040 | |||
| d55b6e0b7a |
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [1.6.2] — 2026-02-27
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Provider labels in Combos** — Combo cards now show user-defined provider names instead of long UUID identifiers, making complex multi-provider combos easier to read ([#121](https://github.com/diegosouzapw/OmniRoute/issues/121))
|
||||
- **Improved request log labels** — RequestLoggerV2 resolves OpenAI-compatible provider IDs to user-defined names via provider nodes lookup
|
||||
- **Smarter API key display** — `formatApiKey()` now shows the full key name for named keys instead of truncating them
|
||||
|
||||
---
|
||||
|
||||
## [1.6.1] — 2026-02-27
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
@@ -759,6 +769,7 @@ New environment variables:
|
||||
|
||||
---
|
||||
|
||||
[1.6.2]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.6.2
|
||||
[1.6.1]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.6.1
|
||||
[1.6.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.6.0
|
||||
[1.5.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.5.0
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "1.6.1",
|
||||
"version": "1.6.2",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -49,6 +49,7 @@ export default function CombosPage() {
|
||||
const notify = useNotificationStore();
|
||||
const [proxyTargetCombo, setProxyTargetCombo] = useState(null);
|
||||
const [proxyConfig, setProxyConfig] = useState(null);
|
||||
const [providerNodes, setProviderNodes] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -60,14 +61,16 @@ export default function CombosPage() {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [combosRes, providersRes, metricsRes] = await Promise.all([
|
||||
const [combosRes, providersRes, metricsRes, nodesRes] = await Promise.all([
|
||||
fetch("/api/combos"),
|
||||
fetch("/api/providers"),
|
||||
fetch("/api/combos/metrics"),
|
||||
fetch("/api/provider-nodes"),
|
||||
]);
|
||||
const combosData = await combosRes.json();
|
||||
const providersData = await providersRes.json();
|
||||
const metricsData = await metricsRes.json();
|
||||
const nodesData = nodesRes.ok ? await nodesRes.json() : { nodes: [] };
|
||||
|
||||
if (combosRes.ok) setCombos(combosData.combos || []);
|
||||
if (providersRes.ok) {
|
||||
@@ -77,6 +80,7 @@ export default function CombosPage() {
|
||||
setActiveProviders(active);
|
||||
}
|
||||
if (metricsRes.ok) setMetrics(metricsData.metrics || {});
|
||||
setProviderNodes(nodesData.nodes || []);
|
||||
} catch (error) {
|
||||
console.log("Error fetching data:", error);
|
||||
} finally {
|
||||
@@ -231,6 +235,7 @@ export default function CombosPage() {
|
||||
key={combo.id}
|
||||
combo={combo}
|
||||
metrics={metrics[combo.name]}
|
||||
providerNodes={providerNodes}
|
||||
copied={copied}
|
||||
onCopy={copy}
|
||||
onEdit={() => setEditingCombo(combo)}
|
||||
@@ -310,6 +315,7 @@ function ComboCard({
|
||||
onProxy,
|
||||
hasProxy,
|
||||
onToggle,
|
||||
providerNodes,
|
||||
}) {
|
||||
const strategy = combo.strategy || "priority";
|
||||
const models = combo.models || [];
|
||||
@@ -317,6 +323,17 @@ function ComboCard({
|
||||
const t = useTranslations("combos");
|
||||
const tc = useTranslations("common");
|
||||
|
||||
// Resolve provider UUID to user-defined name
|
||||
const formatModelDisplay = (modelValue) => {
|
||||
const parts = modelValue.split("/");
|
||||
if (parts.length !== 2) return modelValue;
|
||||
const [providerIdentifier, modelId] = parts;
|
||||
const matchedNode = (providerNodes || []).find(
|
||||
(node) => node.id === providerIdentifier || node.prefix === providerIdentifier
|
||||
);
|
||||
return matchedNode ? `${matchedNode.name}/${modelId}` : modelValue;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card padding="sm" className={`group ${isDisabled ? "opacity-50" : ""}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -381,7 +398,7 @@ function ComboCard({
|
||||
key={index}
|
||||
className="text-[10px] font-mono bg-black/5 dark:bg-white/5 px-1.5 py-0.5 rounded text-text-muted"
|
||||
>
|
||||
{model}
|
||||
{formatModelDisplay(model)}
|
||||
{strategy === "weighted" && weight > 0 ? ` (${weight}%)` : ""}
|
||||
</code>
|
||||
);
|
||||
|
||||
@@ -43,29 +43,32 @@ const DEFAULT_VISIBLE = Object.fromEntries(COLUMNS.map((c) => [c.key, true]));
|
||||
/**
|
||||
* Get a friendly display label for compatible providers.
|
||||
* Converts long IDs like "openai-compatible-chat-02669115-2545-4896-b003-cb4dac09d441"
|
||||
* to readable labels like "OAI-Compat".
|
||||
* to readable labels. If providerNodes are available, uses user-defined name;
|
||||
* otherwise falls back to "OAI-Compat".
|
||||
*/
|
||||
function getProviderDisplayLabel(provider: string): string {
|
||||
function getProviderDisplayLabel(provider: string, providerNodes?: any[]): string {
|
||||
if (!provider) return "-";
|
||||
if (provider.startsWith("openai-compatible-")) {
|
||||
// Extract the "chat" or custom-name part after the prefix
|
||||
const suffix = provider.replace("openai-compatible-", "");
|
||||
// If it's just "chat-<uuid>", show "OAI-Compat"
|
||||
// If it has a meaningful name, include it
|
||||
const parts = suffix.split("-");
|
||||
if (parts.length > 1 && parts[1]?.length >= 8) {
|
||||
// Looks like chat-<uuid>, just show category
|
||||
return `OAI-COMPAT`;
|
||||
if (provider.startsWith("openai-compatible-") || provider.startsWith("anthropic-compatible-")) {
|
||||
// Try to find user-defined name from provider nodes
|
||||
if (providerNodes?.length) {
|
||||
const matchedNode = providerNodes.find(
|
||||
(node) => node.id === provider || node.prefix === provider
|
||||
);
|
||||
if (matchedNode?.name) return matchedNode.name;
|
||||
}
|
||||
return `OAI: ${suffix.slice(0, 16).toUpperCase()}`;
|
||||
}
|
||||
if (provider.startsWith("anthropic-compatible-")) {
|
||||
const suffix = provider.replace("anthropic-compatible-", "");
|
||||
const parts = suffix.split("-");
|
||||
if (parts.length > 1 && parts[1]?.length >= 8) {
|
||||
return `ANT-COMPAT`;
|
||||
// Fallback to generic labels
|
||||
if (provider.startsWith("openai-compatible-")) {
|
||||
const suffix = provider.replace("openai-compatible-", "");
|
||||
const parts = suffix.split("-");
|
||||
if (parts.length > 1 && parts[1]?.length >= 8) return `OAI-COMPAT`;
|
||||
return `OAI: ${suffix.slice(0, 16).toUpperCase()}`;
|
||||
}
|
||||
if (provider.startsWith("anthropic-compatible-")) {
|
||||
const suffix = provider.replace("anthropic-compatible-", "");
|
||||
const parts = suffix.split("-");
|
||||
if (parts.length > 1 && parts[1]?.length >= 8) return `ANT-COMPAT`;
|
||||
return `ANT: ${suffix.slice(0, 16).toUpperCase()}`;
|
||||
}
|
||||
return `ANT: ${suffix.slice(0, 16).toUpperCase()}`;
|
||||
}
|
||||
return null; // Not a compatible provider, use default PROVIDER_COLORS
|
||||
}
|
||||
@@ -90,6 +93,7 @@ export default function RequestLoggerV2() {
|
||||
const [detailData, setDetailData] = useState(null);
|
||||
const intervalRef = useRef(null);
|
||||
const hasLoadedRef = useRef(false);
|
||||
const [providerNodes, setProviderNodes] = useState([]);
|
||||
|
||||
// Column visibility with localStorage persistence
|
||||
const [visibleColumns, setVisibleColumns] = useState(() => {
|
||||
@@ -147,6 +151,14 @@ export default function RequestLoggerV2() {
|
||||
fetchLogs(showLoading);
|
||||
}, [fetchLogs]);
|
||||
|
||||
// Fetch provider nodes for display labels
|
||||
useEffect(() => {
|
||||
fetch("/api/provider-nodes")
|
||||
.then((r) => (r.ok ? r.json() : { nodes: [] }))
|
||||
.then((d) => setProviderNodes(d.nodes || []))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Auto-refresh
|
||||
useEffect(() => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
@@ -299,7 +311,7 @@ export default function RequestLoggerV2() {
|
||||
>
|
||||
<option value="">All Providers</option>
|
||||
{uniqueProviders.map((p) => {
|
||||
const compatLabel = getProviderDisplayLabel(p);
|
||||
const compatLabel = getProviderDisplayLabel(p, providerNodes);
|
||||
const pc = PROVIDER_COLORS[p];
|
||||
return (
|
||||
<option key={p} value={p}>
|
||||
@@ -441,7 +453,7 @@ export default function RequestLoggerV2() {
|
||||
|
||||
{/* Dynamic Provider Quick Filters (from data) */}
|
||||
{uniqueProviders.map((p) => {
|
||||
const compatLabel = getProviderDisplayLabel(p);
|
||||
const compatLabel = getProviderDisplayLabel(p, providerNodes);
|
||||
const pc = PROVIDER_COLORS[p] || {
|
||||
bg: "#374151",
|
||||
text: "#fff",
|
||||
@@ -575,7 +587,7 @@ export default function RequestLoggerV2() {
|
||||
text: "#fff",
|
||||
label: (protocolKey || log.provider || "-").toUpperCase(),
|
||||
};
|
||||
const compatLabel = getProviderDisplayLabel(log.provider);
|
||||
const compatLabel = getProviderDisplayLabel(log.provider, providerNodes);
|
||||
const providerColor = PROVIDER_COLORS[log.provider] || {
|
||||
bg: "#374151",
|
||||
text: "#fff",
|
||||
|
||||
@@ -80,16 +80,16 @@ export function maskAccount(account) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an API key label, masking both name and ID.
|
||||
* Format an API key label, showing full name but masking the ID.
|
||||
* @param {string} apiKeyName - Human-readable name of the key
|
||||
* @param {string} apiKeyId - Unique ID of the key
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatApiKeyLabel(apiKeyName, apiKeyId) {
|
||||
if (!apiKeyName && !apiKeyId) return "—";
|
||||
const maskedName = apiKeyName ? maskSegment(apiKeyName, 2, 1) : "key";
|
||||
if (!apiKeyId) return maskedName;
|
||||
return `${maskedName} (${maskSegment(apiKeyId, 4, 4)})`;
|
||||
const displayName = apiKeyName || "key";
|
||||
if (!apiKeyId) return displayName;
|
||||
return `${displayName} (${maskSegment(apiKeyId, 4, 4)})`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user