diff --git a/CHANGELOG.md b/CHANGELOG.md index 20fd4c49..50f833a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,21 @@ ## [Unreleased] -## [2.4.2] - 2026-03-14 +## [2.4.3] - 2026-03-14 + +> UI polish, routing strategy additions, and graceful error handling for usage limits. + +### ✨ New Features + +- **Fill-First & P2C Routing Strategies**: Added `fill-first` (drain quota before moving on) and `p2c` (Power-of-Two-Choices low-latency selection) to combo strategy picker, with full guidance panels and color-coded badges. +- **Free Stack Preset Models**: Creating a combo with the Free Stack template now auto-fills 7 best-in-class free provider models (Gemini CLI, Kiro, iFlow×2, Qwen, NVIDIA NIM, Groq). Users just activate the providers and get a $0/month combo out-of-the-box. +- **Wider Combo Modal**: Create/Edit combo modal now uses `max-w-4xl` for comfortable editing of large combos. + +### 🐛 Bug Fixes + +- **Limits page HTTP 500 for Codex & GitHub**: `getCodexUsage()` and `getGitHubUsage()` now return a user-friendly message when the provider returns 401/403 (expired token), instead of throwing and causing a 500 error on the Limits page. +- **MaintenanceBanner false-positive**: Banner no longer shows "Server is unreachable" spuriously on page load. Fixed by calling `checkHealth()` immediately on mount and removing stale `show`-state closure. +- **Provider icon tooltips**: Edit (pencil) and delete icon buttons in the provider connection row now have native HTML tooltips — all 6 action icons are now self-documented. > Multiple improvements from community issue analysis, new provider support, bug fixes for token tracking, model routing, and streaming reliability. diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 0bd39ab0..600cf0cb 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 2.4.2 + version: 2.4.3 description: | OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible endpoint that routes requests to multiple AI providers with load balancing, diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 36855dc8..7a36566e 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -161,6 +161,11 @@ async function getGitHubUsage(accessToken, providerSpecificData) { if (!response.ok) { const error = await response.text(); + if (response.status === 401 || response.status === 403) { + return { + message: `GitHub token expired or permission denied. Please re-authenticate the connection.`, + }; + } throw new Error(`GitHub API error: ${error}`); } @@ -620,6 +625,11 @@ async function getCodexUsage(accessToken, providerSpecificData: Record { setStrategy(template.strategy); setConfig((prev) => ({ ...prev, ...template.config })); if (!name.trim()) setName(template.suggestedName); + // Pre-fill Free Stack with 7 real free provider models + if (template.id === "free-stack") { + setModels(FREE_STACK_PRESET_MODELS); + } }; // Format model display name with readable provider name @@ -1473,7 +1525,12 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { return ( <> - +
{/* Name */}
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index b88ea78d..c44f8081 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -2411,6 +2411,7 @@ function ConnectionRow({ @@ -2421,7 +2422,11 @@ function ConnectionRow({ > vpn_lock -
diff --git a/src/shared/components/MaintenanceBanner.tsx b/src/shared/components/MaintenanceBanner.tsx index 9f792b87..fb237431 100644 --- a/src/shared/components/MaintenanceBanner.tsx +++ b/src/shared/components/MaintenanceBanner.tsx @@ -8,38 +8,36 @@ * comes back online. */ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect } from "react"; export default function MaintenanceBanner() { const [show, setShow] = useState(false); const [message, setMessage] = useState(""); - const checkHealth = useCallback(async () => { - try { - const res = await fetch("/api/monitoring/health", { - signal: AbortSignal.timeout(3000), - }); - if (res.ok) { - // Server is healthy — hide banner if shown - if (show) { + useEffect(() => { + const checkHealth = async () => { + try { + const res = await fetch("/api/monitoring/health", { + signal: AbortSignal.timeout(3000), + }); + if (res.ok) { setShow(false); setMessage(""); + } else { + setShow(true); + setMessage("Server is experiencing issues. Some features may be unavailable."); } - } else { + } catch { setShow(true); - setMessage("Server is experiencing issues. Some features may be unavailable."); + setMessage("Server is unreachable. Reconnecting..."); } - } catch { - setShow(true); - setMessage("Server is unreachable. Reconnecting..."); - } - }, [show]); + }; - useEffect(() => { - // Check health every 10 seconds + // Run immediately on mount, then every 10 seconds + checkHealth(); const interval = setInterval(checkHealth, 10000); return () => clearInterval(interval); - }, [checkHealth]); + }, []); // empty deps — checkHealth is defined inside effect, no stale closure if (!show) return null;