diff --git a/.agents/workflows/generate-release.md b/.agents/workflows/generate-release.md index 3a1c6c3c9c..8d0ac8ca17 100644 --- a/.agents/workflows/generate-release.md +++ b/.agents/workflows/generate-release.md @@ -52,19 +52,19 @@ Before creating the release, you must ensure the codebase and supply chain are s git checkout -b release/v2.x.y ``` -### 2. Determine new version +### 2. Determine and sync version -Check current version in `package.json` and increment the **patch** number only: +Check current version in `package.json`: ```bash grep '"version"' package.json ``` -Version format: `2.x.y` — examples: - -- `2.1.2` → `2.1.3` (patch) -- `2.1.9` → `2.1.10` (patch) -- `2.1.10` → `2.2.0` (minor threshold — do manually with `sed`) +> **🔴 BRANCH-VERSION PARITY RULE**: The logical version in `package.json` MUST exactly match the release branch name. For example, if you are on `release/v3.7.0`, the version in `package.json` MUST be `3.7.0`. +> +> - If this is the FIRST time generating a release for a new minor/major branch (e.g., bumping from 3.6.9 to 3.7.0), you MUST ensure the version is bumped to match the new branch logic. +> - If you are just bumping a patch on the current branch (e.g., 3.6.9 to 3.6.10), use: +> `npm version patch --no-git-tag-version` > **⚠️ ATOMIC COMMIT RULE — Version bump MUST happen before committing feature files.** > @@ -97,15 +97,29 @@ npm install ### 4. Finalize CHANGELOG.md -Replace `[Unreleased]` header with the new version and date. -Keep an empty `## [Unreleased]` section above it. +> **🔴 NO MIXUPS RULE**: Ensure you do NOT mix the backlog of the previous version with the new one. The new version section must ONLY contain the features and fixes for the current release. + +Replace the `[Unreleased]` header with the new version and date. +Keep an empty `## [Unreleased]` section above it, separated by a horizontal rule (`---`). ```markdown ## [Unreleased] --- -## [2.x.y] — YYYY-MM-DD +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- ... + +### 🐛 Bug Fixes + +- ... + +--- + +## [3.6.9] — 2026-04-19 ``` ### 5. Update openapi.yaml version ⚠️ MANDATORY diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e7189ab30..4008929740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + ## [3.6.9] — 2026-04-19 ### ✨ New Features diff --git a/README.md b/README.md index 7f45c559c0..351e7e4785 100644 --- a/README.md +++ b/README.md @@ -324,12 +324,13 @@ AI providers can become unstable, return 5xx errors, or hit temporary rate limit **How OmniRoute solves it:** -- **Settings-Driven Lock Hierarchy** — Provider profiles control default account/model lockouts, global model quarantine, and provider circuit breakers from one control surface, while explicit upstream `Retry-After` windows still take priority -- **Exponential Backoff** — Progressive retry delays for both account/model lockouts and higher-level quarantine +- **Request Queue & Pacing** — Per-connection request buckets smooth bursts before they hit upstream rate caps +- **Connection Cooldown** — A single connection cools down after retryable failures with optional upstream `Retry-After` hints and exponential backoff +- **Provider Circuit Breaker** — The provider only trips after fallback is exhausted and the provider request still fails with provider-wide transient errors; connection-scoped `429` rate limits stay in Connection Cooldown +- **Wait For Cooldown** — The server can wait for the earliest connection cooldown to expire and retry the same client request automatically - **Anti-Thundering Herd** — Mutex + semaphore protection against concurrent retry storms - **Combo Fallback Chains** — If the primary provider fails, automatically falls through the chain with no intervention -- **Combo Circuit Breaker** — Auto-disables failing providers within a combo chain -- **Health Dashboard** — Uptime monitoring, circuit breaker states, lockouts, cache stats, p50/p95/p99 latency +- **Health Dashboard** — Uptime monitoring, provider circuit breaker states, cooldowns, cache stats, p50/p95/p99 latency @@ -470,7 +471,7 @@ As request volume grows, without caching the same questions generate duplicate c - **Semantic Cache** — Two-tier cache (signature + semantic) reduces cost and latency - **Request Idempotency** — 5s deduplication window for identical requests - **Rate Limit Detection** — Per-provider RPM, min gap, and max concurrent tracking -- **Editable Rate Limits** — Configurable defaults in Settings → Resilience with persistence +- **Request Queue & Pacing** — Configurable queue, pacing, and concurrency defaults in Settings → Resilience - **API Key Validation Cache** — 3-tier cache for production performance - **Health Dashboard with Telemetry** — p50/p95/p99 latency, cache stats, uptime @@ -567,8 +568,8 @@ Teams need quick runtime changes during incidents or cost events. **How OmniRoute solves it:** - Switch combo activation directly from MCP dashboard -- Apply resilience profiles from pre-defined policy packs -- Reset circuit breaker state from the same operations panel +- Tune queue, cooldown, breaker, and wait settings from the dedicated Resilience page +- Review live provider breaker state from the Health dashboard @@ -1352,7 +1353,7 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | 🔢 **Hybrid Token Counting** | Uses provider-side `/messages/count_tokens` when available; falls back to estimation — accurate usage tracking without guessing | | 🌱 **Model Alias Auto-Seed** | 30+ cross-proxy dialect aliases normalised at startup — no more routing mismatches | | 🛡️ **Safe Outbound Fetch** | All provider validation and model discovery go through a guarded fetch layer blocking private/local URLs with retry, timeout, and SSRF protection | -| 🔄 **Cooldown-Aware Retries** | Chat requests auto-retry on model-scoped cooldowns with configurable `requestRetry` and `maxRetryIntervalSec` | +| ⏳ **Wait For Cooldown** | Server-side chat retries when every candidate connection is cooling down; configurable `enabled`, `maxRetries`, and `maxRetryWaitSec` | | 🔍 **Runtime Env Validation** | Startup validates all env vars with Zod schemas — clear errors for missing secrets, invalid URLs, or wrong types | | 📋 **Compliance Audit Expansion** | Structured audit logs with pagination, request context, auth events, provider CRUD events, and SSRF-blocked validation logging | | 🔐 **TPS Log Metric** | Log details modal shows Tokens Per Second (TPS) — quick performance at-a-glance for every request | @@ -1406,7 +1407,7 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | 📡 **A2A Task Lifecycle Management** | List/filter tasks, inspect events/artifacts, cancel running tasks | | 📋 **Agent Card Discovery** | `/.well-known/agent.json` for client auto-discovery | | 🧪 **Protocol E2E Test Harness** | Real MCP SDK + A2A client flows in `test:protocols:e2e` | -| ⚙️ **Operational Controls** | Switch combo, apply resilience profiles, reset breakers from one control surface | +| ⚙️ **Operational Controls** | Switch combos, tune resilience settings, and review breaker state from dedicated Health and Settings surfaces | ### 🧠 Routing & Intelligence @@ -1446,35 +1447,38 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. ### 🛡️ Resilience, Security & Governance -| Feature | What It Does | -| ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | -| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | -| 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | -| 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | -| ⚡ **Request Idempotency** | Duplicate protection window | -| 🔒 **TLS Fingerprint Spoofing** | Browser-like TLS fingerprint — **reduces bot detection and account flagging** | -| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** | -| 🌐 **IP Filtering** | Allowlist/blocklist control for exposed deployments | -| 📊 **Editable Rate Limits** | Configurable global/provider-level limits with persistence | -| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations | -| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks | -| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures | -| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically | -| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls | -| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` | -| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog | -| 🛡️ **Safe Outbound Fetch** 🆕 | Guarded fetch for provider calls — blocks private/local URLs, retries, SSRF protection | -| 🔄 **Cooldown-Aware Retries** 🆕 | Auto-retry chat on model cooldowns; configurable `requestRetry` / `maxRetryIntervalSec` | -| 🔍 **Runtime Env Validation** 🆕 | Zod-based env schema validation at startup with actionable error messages | -| 📋 **Compliance Audit v2** 🆕 | Pagination, request context, auth events, provider CRUD, and SSRF-blocked logging | +| Feature | What It Does | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------- | +| 🔌 **Provider Circuit Breakers** | Provider-wide trip/recover after fallback exhaustion with configurable thresholds | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | +| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | +| 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | +| 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | +| ⚡ **Request Idempotency** | Duplicate protection window | +| 🔒 **TLS Fingerprint Spoofing** | Browser-like TLS fingerprint — **reduces bot detection and account flagging** | +| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** | +| 🌐 **IP Filtering** | Allowlist/blocklist control for exposed deployments | +| 🚦 **Request Queue & Pacing** | Configurable per-connection request buckets for RPM, spacing, concurrency, and max wait | +| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations | +| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks | +| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures | +| ❄️ **Connection Cooldown** | Retryable 408/429/5xx failures cool down a single connection with optional upstream hints | +| 🚪 **Auto-Disable Banned Accounts** | Permanently blocked token accounts can be disabled automatically | +| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls | +| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` | +| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog | +| 🛡️ **Safe Outbound Fetch** 🆕 | Guarded fetch for provider calls — blocks private/local URLs, retries, SSRF protection | +| ⏳ **Wait For Cooldown** 🆕 | Auto-retry chat after connection cooldowns; configurable `enabled`, `maxRetries`, and `maxRetryWaitSec` | +| 🔍 **Runtime Env Validation** 🆕 | Zod-based env schema validation at startup with actionable error messages | +| 📋 **Compliance Audit v2** 🆕 | Pagination, request context, auth events, provider CRUD, and SSRF-blocked logging | ### 📊 Observability & Analytics | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | @@ -2281,7 +2285,7 @@ OmniRoute has **218+ features planned** across multiple development phases. Here | 🧠 **Routing & Intelligence** | 25+ | Lowest-latency routing, tag-based routing, quota preflight, quota-aware P2C, step-based combo routing | | 🔒 **Security & Compliance** | 20+ | SSRF hardening, credential cloaking, rate-limit per endpoint, management key scoping | | 📊 **Observability** | 15+ | OpenTelemetry integration, real-time quota monitoring, combo target health, cost tracking per model | -| 🔄 **Provider Integrations** | 20+ | Dynamic model registry, provider cooldowns, multi-account Codex, Copilot quota parsing | +| 🔄 **Provider Integrations** | 20+ | Dynamic model registry, connection cooldowns, multi-account Codex, Copilot quota parsing | | ⚡ **Performance** | 15+ | Dual cache layer, prompt cache, response cache, streaming keepalive, batch API | | 🌐 **Ecosystem** | 10+ | WebSocket API, config hot-reload, distributed config store, commercial mode | diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 376ceecacf..087d5619f3 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -284,12 +284,12 @@ GET response includes `agents[]` (id, name, binary, version, installed, protocol ### Resilience & Rate Limits -| Endpoint | Method | Description | -| ----------------------- | --------- | ------------------------------- | -| `/api/resilience` | GET/PATCH | Get/update resilience profiles | -| `/api/resilience/reset` | POST | Reset circuit breakers | -| `/api/rate-limits` | GET | Per-account rate limit status | -| `/api/rate-limit` | GET | Global rate limit configuration | +| Endpoint | Method | Description | +| ----------------------- | --------- | ---------------------------------------------------------------------------------- | +| `/api/resilience` | GET/PATCH | Get/update request queue, connection cooldown, provider breaker, and wait settings | +| `/api/resilience/reset` | POST | Reset provider circuit breakers | +| `/api/rate-limits` | GET | Per-account rate limit status | +| `/api/rate-limit` | GET | Global rate limit configuration | ### Evals @@ -443,25 +443,6 @@ Content-Type: application/json } ``` ---- - -## Model Availability - -```bash -# Get real-time model availability across all providers -GET /api/models/availability - -# Check availability for a specific model -POST /api/models/availability -Content-Type: application/json - -{ - "model": "claude-sonnet-4-5-20250929" -} -``` - ---- - ## Request Processing 1. Client sends request to `/v1/*` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7900209f28..37c8b1baf0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -42,7 +42,7 @@ Core capabilities: - Circuit breaker pattern for provider resilience - Anti-thundering herd protection with mutex locking - Signature-based request deduplication cache -- Domain layer: model availability, cost rules, fallback policy, lockout policy +- Domain layer: cost rules, fallback policy, lockout policy - Context Relay: session handoff summaries for account rotation continuity - Domain state persistence (SQLite write-through cache for fallbacks, budgets, lockouts, circuit breakers) - Policy engine for centralized request evaluation (lockout → budget → fallback) @@ -51,7 +51,7 @@ Core capabilities: - Correlation ID (X-Request-Id) for end-to-end tracing - Compliance audit logging with opt-out per API key - Eval framework for LLM quality assurance -- Resilience UI dashboard with real-time circuit breaker status +- Health dashboard with real-time provider circuit breaker status - MCP Server (25 tools) with 3 transports (stdio/SSE/Streamable HTTP) - A2A Server (JSON-RPC 2.0 + SSE) with skills and task lifecycle - Memory system (extraction, injection, retrieval, summarization) @@ -205,10 +205,9 @@ Management domains: - System prompt: `src/app/api/settings/system-prompt` (GET/PUT) - Sessions: `src/app/api/sessions` (GET) - Rate limits: `src/app/api/rate-limits` (GET) -- Resilience: `src/app/api/resilience` (GET/PATCH) — provider profiles, circuit breaker, rate limit state -- Resilience reset: `src/app/api/resilience/reset` (POST) — reset breakers + cooldowns +- Resilience: `src/app/api/resilience` (GET/PATCH) — request queue, connection cooldown, provider breaker, wait-for-cooldown config +- Resilience reset: `src/app/api/resilience/reset` (POST) — reset provider breakers - Cache stats: `src/app/api/cache/stats` (GET/DELETE) -- Model availability: `src/app/api/models/availability` (GET/POST) - Telemetry: `src/app/api/telemetry/summary` (GET) - Budget: `src/app/api/usage/budget` (GET/POST) - Fallback chains: `src/app/api/fallback/chains` (GET/POST/DELETE) @@ -265,7 +264,6 @@ Services (business logic): Domain layer modules: -- Model availability: `src/lib/domain/modelAvailability.ts` - Cost rules/budgets: `src/lib/domain/costRules.ts` - Fallback policy: `src/lib/domain/fallbackPolicy.ts` - Combo resolver: `src/lib/domain/comboResolver.ts` @@ -794,7 +792,7 @@ legacy compatibility. The current runtime contract uses: ## 1) Account/Provider Availability -- provider account cooldown on transient/rate/auth errors +- connection cooldown on retryable upstream failures - account fallback before failing request - combo model fallback when current model/provider path is exhausted @@ -876,7 +874,7 @@ Environment variables actively used by code: 5. The `open-sse/` directory is published as the `@omniroute/open-sse` **npm workspace package**. Source code imports it via `@omniroute/open-sse/...` (resolved by Next.js `transpilePackages`). File paths in this document still use the directory name `open-sse/` for consistency. 6. Charts in the dashboard use **Recharts** (SVG-based) for accessible, interactive analytics visualizations (model usage bar charts, provider breakdown tables with success rates). 7. E2E tests use **Playwright** (`tests/e2e/`), run via `npm run test:e2e`. Unit tests use **Node.js test runner** (`tests/unit/`), run via `npm run test:unit`. Source code under `src/` is **TypeScript** (`.ts`/`.tsx`); the `open-sse/` workspace remains JavaScript (`.js`). -8. Settings page is organized into 5 tabs: Security, Routing (6 global strategies: fill-first, round-robin, p2c, random, least-used, cost-optimized), Resilience (editable rate limits, circuit breaker, policies, **Context Relay** handoff config), AI (thinking budget, system prompt, prompt cache), Advanced (proxy). +8. Settings page is organized into 7 tabs: General, Appearance, AI, Security, Routing, Resilience, Advanced. The Resilience page only configures request queue, connection cooldown, provider breaker, and wait-for-cooldown behavior; live breaker runtime state is shown on the Health page. 9. **Context Relay** strategy (`context-relay`) is split across two layers: `combo.ts` decides if a handoff should be generated, `chat.ts` injects the handoff after account resolution. Handoff data lives in `context_handoffs` SQLite table. This split is intentional because only `chat.ts` knows whether the actual account changed. 10. **Proxy enforcement** is now comprehensive: `tokenHealthCheck.ts` resolves proxy per connection, `/api/providers/validate` uses `runWithProxyContext`, and `proxyFetch.ts` uses `undici.fetch()` to maintain dispatcher compatibility on Node 22. 11. **Node.js runtime policy detection**: `/api/settings/require-login` returns `nodeVersion` and `nodeCompatible` fields. The login page renders a warning banner when the runtime falls outside the supported secure Node.js lines. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index a639755c1d..b1f180b531 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -756,35 +756,34 @@ Chain: production-fallback Configure via **Dashboard → Settings → Resilience**. -OmniRoute implements provider-level resilience with four components: +OmniRoute implements provider-level resilience with five components: -1. **Provider Profiles** — Per-provider configuration for: - - **Transient Cooldown** — Base cooldown for transient upstream failures - - **Rate Limit Cooldown** — Base cooldown for `429`-driven lockouts - - **Max Backoff Level** — Maximum exponential backoff level for repeated failures - - **CB Threshold** — Failure count before model quarantine / provider circuit breaker escalates - - **CB Reset Time** — Failure counting window and breaker reset timer - -2. **Editable Rate Limits** — System-level defaults configurable in the dashboard: +1. **Request Queue & Pacing** — System-level request shaping: - **Requests Per Minute (RPM)** — Maximum requests per minute per account - **Min Time Between Requests** — Minimum gap in milliseconds between requests - **Max Concurrent Requests** — Maximum simultaneous requests per account - - Click **Edit** to modify, then **Save** or **Cancel**. Values persist via the resilience API. -3. **Circuit Breaker** — Tracks failures per provider and automatically opens the circuit when the configured threshold is reached: +2. **Connection Cooldown** — Per-auth-type configuration for a single connection after retryable failures: + - **Base Cooldown** — Default cooldown window for retryable upstream failures + - **Use Upstream Retry Hints** — Honors authoritative `Retry-After` or reset hints when provided + - **Max Backoff Steps** — Maximum exponential backoff level for repeated failures + +3. **Provider Circuit Breaker** — Tracks end-to-end provider failures and automatically opens the breaker when the configured threshold is reached: + - **Failure Threshold** — Consecutive provider failures before opening the breaker + - **Reset Timeout** — Time window before the provider is tested again - **CLOSED** (Healthy) — Requests flow normally - **OPEN** — Provider is temporarily blocked after repeated failures - **HALF_OPEN** — Testing if provider has recovered - The same provider profile also drives model-scoped lockouts: - - Account/model lockouts react immediately to authoritative `429` / `404` signals and use the configured cooldown + backoff values - - Global provider/model quarantine only activates after repeated exhaustion hits the configured **CB Threshold** within **CB Reset Time** + Connection-scoped `429` rate limits stay in **Connection Cooldown** and do not count toward the provider breaker. -4. **Policies & Locked Identifiers** — Shows circuit breaker status and locked identifiers with force-unlock capability. + The provider breaker runtime state is shown on **Dashboard → Health** only. -5. **Rate Limit Auto-Detection** — Monitors `429` and `Retry-After` headers to proactively avoid hitting provider rate limits. When an upstream provider returns an explicit wait window, that authoritative `Retry-After` value overrides the base cooldown from the provider profile. +4. **Wait For Cooldown** — If every candidate connection is already cooling down, OmniRoute can wait for the earliest cooldown and retry the same client request automatically. -**Pro Tip:** Use **Reset All** button to clear all circuit breakers and cooldowns when a provider recovers from an outage. +5. **Rate Limit Auto-Detection** — When upstream providers return explicit wait windows, those hints override the local connection cooldown when the setting is enabled. + +**Pro Tip:** Use the **Health** page to inspect and reset live provider breakers after an outage. The Resilience page only changes configuration. --- @@ -824,14 +823,14 @@ curl -X POST http://localhost:20128/api/db-backups/import \ The settings page is organized into 6 tabs for easy navigation: -| Tab | Contents | -| -------------- | ---------------------------------------------------------------------------------------------- | -| **General** | System storage tools, appearance settings, theme controls, and per-item sidebar visibility | -| **Security** | Login/Password settings, IP Access Control, API auth for `/models`, and Provider Blocking | -| **Routing** | Global routing strategy (6 options), wildcard model aliases, fallback chains, combo defaults | -| **Resilience** | Provider profiles, editable rate limits, circuit breaker status, policies & locked identifiers | -| **AI** | Thinking budget configuration, global system prompt injection, prompt cache stats | -| **Advanced** | Global proxy configuration (HTTP/SOCKS5) | +| Tab | Contents | +| -------------- | -------------------------------------------------------------------------------------------- | +| **General** | System storage tools, appearance settings, theme controls, and per-item sidebar visibility | +| **Security** | Login/Password settings, IP Access Control, API auth for `/models`, and Provider Blocking | +| **Routing** | Global routing strategy (6 options), wildcard model aliases, fallback chains, combo defaults | +| **Resilience** | Request queue, connection cooldown, provider breaker config, and wait-for-cooldown behavior | +| **AI** | Thinking budget configuration, global system prompt injection, prompt cache stats | +| **Advanced** | Global proxy configuration (HTTP/SOCKS5) | --- @@ -904,9 +903,9 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car | Card | What It Shows | | --------------------- | ----------------------------------------------------------- | | **System Status** | Uptime, version, memory usage, data directory | -| **Provider Health** | Per-provider circuit breaker state (Closed/Open/Half-Open) | -| **Rate Limits** | Active rate limit cooldowns per account with remaining time | -| **Active Lockouts** | Providers temporarily blocked by the lockout policy | +| **Provider Health** | Global provider circuit breaker runtime state | +| **Rate Limits** | Active connection cooldowns per account with remaining time | +| **Active Lockouts** | Active model-scoped lockouts and temporary exclusions | | **Signature Cache** | Deduplication cache stats (active keys, hit rate) | | **Latency Telemetry** | p50/p95/p99 latency aggregation per provider | diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index 6a4371a236..11e054045c 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/ar/README.md b/docs/i18n/ar/README.md index 4526b5e067..97cfddac52 100644 --- a/docs/i18n/ar/README.md +++ b/docs/i18n/ar/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index aac87fb713..d82f894ada 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/bg/README.md b/docs/i18n/bg/README.md index d71ddae240..1c8069e215 100644 --- a/docs/i18n/bg/README.md +++ b/docs/i18n/bg/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index 8ae321a1b8..9ec5eef036 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/cs/README.md b/docs/i18n/cs/README.md index 7354aaac51..72da7845d6 100644 --- a/docs/i18n/cs/README.md +++ b/docs/i18n/cs/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index 0217255bc7..5c82121780 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/da/README.md b/docs/i18n/da/README.md index b8f451b6a4..e21fd34988 100644 --- a/docs/i18n/da/README.md +++ b/docs/i18n/da/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index 35fd5cd31a..d3655cf26f 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/de/README.md b/docs/i18n/de/README.md index ea2bf00953..a8fd113b57 100644 --- a/docs/i18n/de/README.md +++ b/docs/i18n/de/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index c72ad7b37d..685b8dc4ce 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/es/README.md b/docs/i18n/es/README.md index 3182236a1d..bee2ae5c3f 100644 --- a/docs/i18n/es/README.md +++ b/docs/i18n/es/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index 6ba4b4c04d..f8770e95d2 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/fi/README.md b/docs/i18n/fi/README.md index cf3309061d..370460fc7a 100644 --- a/docs/i18n/fi/README.md +++ b/docs/i18n/fi/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index d05b6edc81..28e2d5b90a 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/fr/README.md b/docs/i18n/fr/README.md index 802f3981f6..74f16ade68 100644 --- a/docs/i18n/fr/README.md +++ b/docs/i18n/fr/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index 45f9035a05..2083b23674 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/he/README.md b/docs/i18n/he/README.md index 1739d0b60c..711460f293 100644 --- a/docs/i18n/he/README.md +++ b/docs/i18n/he/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index 21da9b5cb4..bf0b19f322 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/hi/README.md b/docs/i18n/hi/README.md index b4816fd70b..eb7515428d 100644 --- a/docs/i18n/hi/README.md +++ b/docs/i18n/hi/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index 056b2ff49f..f89a89b20e 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/hu/README.md b/docs/i18n/hu/README.md index cf45ce4aff..84b4fff99f 100644 --- a/docs/i18n/hu/README.md +++ b/docs/i18n/hu/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index eb5354a9de..8165bcb25b 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/id/README.md b/docs/i18n/id/README.md index c0f59bce46..2b15be7354 100644 --- a/docs/i18n/id/README.md +++ b/docs/i18n/id/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md index 8808920e72..ff7d835e62 100644 --- a/docs/i18n/in/CHANGELOG.md +++ b/docs/i18n/in/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/in/README.md b/docs/i18n/in/README.md index d62cdae81d..b314077223 100644 --- a/docs/i18n/in/README.md +++ b/docs/i18n/in/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index 7c37720bc6..deb5953f69 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/it/README.md b/docs/i18n/it/README.md index 9b8eb9c244..3abd480ca9 100644 --- a/docs/i18n/it/README.md +++ b/docs/i18n/it/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index 9b5b00b7d6..cadc15fb4c 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/ja/README.md b/docs/i18n/ja/README.md index eab2780fcc..79f2a33aee 100644 --- a/docs/i18n/ja/README.md +++ b/docs/i18n/ja/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index e5c11bdfa2..8ca32f5bc7 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/ko/README.md b/docs/i18n/ko/README.md index 8bd314cd7c..caf3171d19 100644 --- a/docs/i18n/ko/README.md +++ b/docs/i18n/ko/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index 9d00dde97b..5509f3b2c7 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/ms/README.md b/docs/i18n/ms/README.md index d7a8463d42..846f22fbd4 100644 --- a/docs/i18n/ms/README.md +++ b/docs/i18n/ms/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index b25da82b8f..dd26a3d0a1 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/nl/README.md b/docs/i18n/nl/README.md index 78d4887f89..dead5c9c93 100644 --- a/docs/i18n/nl/README.md +++ b/docs/i18n/nl/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index 43bd234c88..9a33ab11a1 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/no/README.md b/docs/i18n/no/README.md index d51967b29f..cd848c3d18 100644 --- a/docs/i18n/no/README.md +++ b/docs/i18n/no/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index b06b6abd9b..e8014248a7 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/phi/README.md b/docs/i18n/phi/README.md index cad42b780a..3982f91db7 100644 --- a/docs/i18n/phi/README.md +++ b/docs/i18n/phi/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index d8bb54352b..e9c24b233c 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/pl/README.md b/docs/i18n/pl/README.md index cd9b2b3f1d..3521216f16 100644 --- a/docs/i18n/pl/README.md +++ b/docs/i18n/pl/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index 7a65b3219d..8cc223bb43 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/pt-BR/README.md b/docs/i18n/pt-BR/README.md index 343d7313d5..df56ff3fb4 100644 --- a/docs/i18n/pt-BR/README.md +++ b/docs/i18n/pt-BR/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index 29a7203ccc..a5523ab6ab 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/pt/README.md b/docs/i18n/pt/README.md index 978e95d699..e758ec7301 100644 --- a/docs/i18n/pt/README.md +++ b/docs/i18n/pt/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index b6a5f585dd..17bde222f4 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/ro/README.md b/docs/i18n/ro/README.md index 3b72636485..417178a60c 100644 --- a/docs/i18n/ro/README.md +++ b/docs/i18n/ro/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index e3a4c049f2..190102f371 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/ru/README.md b/docs/i18n/ru/README.md index f27613dd69..f542d47b2d 100644 --- a/docs/i18n/ru/README.md +++ b/docs/i18n/ru/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index bd73a04f84..9d90a7e731 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/sk/README.md b/docs/i18n/sk/README.md index 95c87dda68..436e74d980 100644 --- a/docs/i18n/sk/README.md +++ b/docs/i18n/sk/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index 79b402a6f9..7ae8745a84 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/sv/README.md b/docs/i18n/sv/README.md index 9377c8b9c6..b00b03fa02 100644 --- a/docs/i18n/sv/README.md +++ b/docs/i18n/sv/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index 84fe853b7f..9295a7f6b4 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/th/README.md b/docs/i18n/th/README.md index 08652329d9..7d516632eb 100644 --- a/docs/i18n/th/README.md +++ b/docs/i18n/th/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index a71fdd7fab..a3f3833f09 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/tr/README.md b/docs/i18n/tr/README.md index c048afcf2a..31abf535c8 100644 --- a/docs/i18n/tr/README.md +++ b/docs/i18n/tr/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index 163adbbfff..8a8c7c9a28 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/uk-UA/README.md b/docs/i18n/uk-UA/README.md index 6d80b687ef..dbe51a1e92 100644 --- a/docs/i18n/uk-UA/README.md +++ b/docs/i18n/uk-UA/README.md @@ -2,7 +2,7 @@ ### Ніколи не припиняйте кодувати. Розумна маршрутизація до **БЕЗКОШТОВНИХ та недорогих AI моделей** з автоматичним резервуванням. -_Ваш універсальний API проксі — одна кінцева точка, 100+ провайдерів, нульовий простій. Тепер з **MCP Server (25 інструментів)**, **A2A Protocol**, **Memory/Skills Systems** та **Electron Desktop App**._ +_Ваш універсальний API проксі — одна кінцева точка, 100+ провайдерів, нульовий простій. Тепер з **MCP Server (34 інструменти)**, **A2A Protocol**, **Memory/Skills Systems** та **Electron Desktop App**._ **Chat Completions • Embeddings • Image Generation • Video • Music • Audio • Reranking • **Web Search** • MCP Server • A2A Protocol • 100% TypeScript** @@ -1449,7 +1449,8 @@ OmniRoute v3.6 побудований як операційна платформ | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1475,7 +1476,8 @@ OmniRoute v3.6 побудований як операційна платформ | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index d2e3ad4d37..113564021a 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/vi/README.md b/docs/i18n/vi/README.md index 1f5ab6328c..24c986ad9b 100644 --- a/docs/i18n/vi/README.md +++ b/docs/i18n/vi/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index e16a515aa1..5d75dce988 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -8,6 +8,91 @@ --- +## [3.7.0] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430) +- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430) +- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430) +- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430) + +### 🐛 Bug Fixes + +- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438) +- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164) + +--- + +## [3.6.9] — 2026-04-19 + +### ✨ New Features + +- **feat(providers):** Mark Qwen OAuth provider as deprecated following the upstream free tier shutdown on 2026-04-15. Adds deprecation warning to CLI tool UI and rewrites `saveQwenConfig` to inject OmniRoute as a multi-provider (openai, anthropic, gemini) via `.qwen/settings.json` and `.qwen/.env` (#1437) +- **feat(cc-compatible):** Align Claude Code-compatible request shape with the official Claude CLI protocol, including proper system skeleton and request normalization (#1411) +- **feat(skills):** Provider-aware marketplace UX with scored AUTO injection and memory pipeline hardening. Skills now show relevance scores and can automatically inject context into requests (#1411) +- **feat(claude-code):** Update Claude Code obfuscation to version 2.1.114, centralize hardcoded version strings, and use standard logger (#1403) +- **feat(cli-tools):** Add direct configuration file generation and override support for Qwen Code local settings (#1394) +- **feat(providers):** Derive Claude CLI model defaults dynamically from provider registry to stay current with upstream API changes (#1393) +- **feat(core):** Implement persistent API key, backup pruning, and GPU optimization (#1350, #1367, #1369) + +### 🐛 Bug Fixes + +- **fix(cli-tools):** Prevent masked API keys (`sk-31c4****8600`) from being written to CLI tool config files. The dashboard UI now passes `key.id` to the backend, which resolves the unmasked key from the database via a new `resolveApiKey()` helper. Fixes auth failures across all CLI tools (Claude, Codex, Cline, Kilo, Droid, OpenClaw, Antigravity) (#1435) +- **fix(cc-compatible):** Trim the default Claude Code-compatible system prompt skeleton from a multi-paragraph instruction set down to a single identifier line, reducing redundant token usage since Claude Code already injects its own extensive system context (#1433) +- **fix(security):** Resolve SSRF environment static evaluation bug where the outbound URL guard could be bypassed via computed expressions (#1427) +- **fix(auth):** Reload fresh token state and unify expiry persistence to prevent stale credentials from causing cascading auth failures +- **fix(core):** Stabilization fixes for token refresh, usage translation, and testing infrastructure +- **fix(api):** Stop sending unsupported parameters to Gemini and Codex upstream APIs, preventing 400 Bad Request errors +- **fix(skills):** Optimize AUTO scoring algorithm and include Responses API input context for more accurate skill relevance matching (#1418) +- **fix(responses):** Preserve reasoning content when translating Chat Completions format to Responses API format, preventing loss of chain-of-thought data (#1414) +- **fix(cc-compatible):** Add Claude CLI system skeleton for OpenAI-format inputs to ensure consistent behavior when CC-compatible providers receive OpenAI-style payloads +- **fix(providers):** Add `ref` to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` to fix 400 errors from Gemini CLI when tool schemas contain JSON Schema `$ref` fields +- **fix(codex):** Prevent proactive token refresh from consuming valid tokens and strip the unsupported `background` parameter from upstream requests +- **fix(providers):** Fix `usage.prompt_tokens` under-reporting when translating Claude caching responses to OpenAI format (#1426) +- **fix(core):** Fix token refresh resilience for Codex providers. Unrecoverable OAuth refresh errors (`token_expired` and `invalid_token`) now correctly mark the connection as invalid to prompt user re-authentication, rather than silently failing (#1415) +- **fix(providers):** Fix Gemini tool calling by removing the unsupported `additionalProperties` schema field, resolving 400 errors during complex tool invocations (#1421) +- **fix(providers):** Remove arbitrary user thought signature injection in Gemini responses to comply with updated API constraints (#1410) +- **fix(providers):** Fix Gemini API part count mismatch for streaming responses (#1412) +- **fix(codex):** Respect `openaiStoreEnabled` setting during native passthrough for Responses API to prevent unsupported upstream arguments (#1432) +- **fix(ui):** Makes dropdown text visible in dark mode within the Combo Builder modal (#1409) +- **fix(chatcore):** Apply proactive compression before provider translation to prevent token limit errors in combo routes (#1406) +- **fix(claude-code):** Scope thinking stripping to executor boundaries to prevent issues with normal API requests (#1401) +- **fix(claude-code):** Scope obfuscation logic to CLI clients only and fix associated test assertions +- **fix(mitm):** Resolve MITM not working when connecting Antigravity (#1399) +- **fix(security):** Resolve CodeQL password hash alert and fix TruffleHog CI failure (#161) +- **fix(combo):** Fallback to the next model when all provider accounts return a 503 rate-limited signal instead of aborting the routing sequence (#1398) +- **fix(codex):** Strip server-generated IDs from response items in input to prevent 404 lookup errors in multi-turn Codex Conversations (#1397) +- **fix(codex):** Optimize Chat Completions paths by converting `system` to `developer` roles instead of hoisting them into instructions, enabling prompt caching for system messages on GPT-5 models (#1400) +- **fix(providers):** Resolve Claude passthrough corruption (#1359), Kimi-k2 reasoning header rejections (#1360), thinking parameter leaks (#1361), and Ollama proxy redirect drops (#1381) +- **fix(core):** Proxy lookup in key validation respects the new ProxyRegistry environments, and proxy contexts correctly inherit downwards during token refresh preventing expiration loops (#1384, #1390) +- **fix(providers):** Treat upstream legacy validation HTTP 5xx responses as a valid bypass for Qoder PAT tokens to prevent false negative invalidation (#1391) +- **fix(electron):** Resolve type error in Header electronAPI properties +- **fix(security):** Resolve CodeQL security alerts including safe prototype bindings (#151, #152, #154, #155-159) +- **fix(tsc):** Silence `baseUrl` deprecation warnings for TypeScript 5.5+ configurations + +### 🧪 Tests + +- **test(core):** Resolve typescript strictness complaints and fix combo-routing-engine test regression +- **test(core):** Resolve remaining strict type errors across all unit test files +- **test(providers):** Fix provider service assertion for anthropic-compatible header format +- **test(codex):** Align codex passthrough assertions with explicit store retention policy +- **test(codex):** Fix store assertion for codex responses +- **test(cli):** Resolve strict null checks in Qoder unit tests + +### 🛠️ Maintenance + +- **chore:** Sync infrastructure with docker postinstall components and secondary CodeQL analysis rules +- **chore:** Enforce contributor credit rule in review-prs workflow +- **chore:** Fix TS errors and update review-prs workflow for improved automation +- **ci:** Allow manual CI dispatch for release branches +- **ci:** Shard long-running test suites and relax timeouts for stability +- **ci:** Restore release v3.6.9 build pipeline and fix flaky tests +- **docs:** Update generate-release workflow to use full changelog for PR body +- **docs:** Enforce PR merge instead of manual close in workflows + +--- + ## [3.6.8] — 2026-04-17 ### ✨ New Features diff --git a/docs/i18n/zh-CN/README.md b/docs/i18n/zh-CN/README.md index 15a8a93b01..d55fde8310 100644 --- a/docs/i18n/zh-CN/README.md +++ b/docs/i18n/zh-CN/README.md @@ -1452,7 +1452,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | ----------------------------------- | --------------------------------------------------------------------------------------- | -| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls | +| 🔌 **Circuit Breakers** | Per-provider and per-model trip/recover with 10-minute cooldowns | +| 🔒 **Daily Quota Lock** 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight | | 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format | | 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events | | 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers | @@ -1478,7 +1479,8 @@ OmniRoute v3.6 is built as an operational platform, not just a relay proxy. | Feature | What It Does | | -------------------------------- | ----------------------------------------------------- | | 📝 **Request + Proxy Logging** | Full request/response and proxy logging | -| 📉 **Streamed Detailed Logs** 🆕 | Reconstructs SSE payload streams cleanly into the UI | +| 📉 **Streamed Detailed Logs** | Reconstructs SSE payload streams cleanly into the UI | +| 🏷️ **Real-Time Model Badges** 🆕 | Live model status and daily quota countdown timers | | 📋 **Unified Logs Dashboard** | Request, proxy, audit, and console views in one page | | 🔍 **Request Telemetry** | p50/p95/p99 latency and request tracing | | 🏥 **Health Dashboard** | Uptime, breaker states, lockouts, cache stats | diff --git a/docs/openapi.yaml b/docs/openapi.yaml index ac43b5671a..480e803ab2 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.6.9 + version: 3.7.0 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, @@ -460,22 +460,6 @@ paths: "200": description: Complete catalog with all providers - /api/models/availability: - get: - tags: [Models] - summary: Get model availability status - description: Returns availability data for all configured models across providers. - responses: - "200": - description: Model availability map - post: - tags: [Models] - summary: Refresh model availability - description: Triggers a re-check of model availability across all providers. - responses: - "200": - description: Availability refreshed - # ─── Management Endpoints ────────────────────────────────────── /api/providers: @@ -1844,13 +1828,13 @@ paths: /api/resilience: get: tags: [System] - summary: Get resilience profiles and circuit breaker states + summary: Get resilience configuration responses: "200": - description: Resilience configuration and current states - put: + description: Request queue, connection cooldown, provider breaker, and wait settings + patch: tags: [System] - summary: Update resilience profiles + summary: Update resilience configuration requestBody: required: true content: @@ -1859,7 +1843,7 @@ paths: type: object responses: "200": - description: Updated resilience profiles + description: Updated resilience configuration /api/resilience/reset: post: diff --git a/electron/package.json b/electron/package.json index 22a1a2b76c..f4da5e1509 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "omniroute-desktop", - "version": "3.6.9", + "version": "3.7.0", "description": "OmniRoute Desktop Application", "main": "main.js", "author": { diff --git a/next.config.mjs b/next.config.mjs index db1017ae84..375b966e96 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -28,6 +28,7 @@ const nextConfig = { "./playwright-report/**/*", "./app.__qa_backup/**/*", "./tests/**/*", + "./logs/**/*", ], }, serverExternalPackages: [ diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index b48b3dc9d9..ede6e0cdff 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -1400,6 +1400,25 @@ export const REGISTRY: Record = { passthroughModels: true, }, + modelscope: { + id: "modelscope", + alias: "ms", + format: "openai", + executor: "default", + baseUrl: "https://api-inference.modelscope.cn/v1/chat/completions", + authType: "apikey", + authHeader: "bearer", + // ModelScope uses per-model quotas. Setting passthroughModels: true ensures 429/404 + // only locks the specific model, not the entire connection. This allows fallback + // to other models on the same API key. + passthroughModels: true, + models: [ + { id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5" }, + { id: "ZhipuAI/GLM-5", name: "GLM-5" }, + { id: "stepfun-ai/Step-3.5-Flash", name: "Step-3.5-Flash" }, + ], + }, + // ── New Free Providers (2026) ───────────────────────────────────────────── longcat: { @@ -2128,8 +2147,14 @@ export function getUnsupportedParams(provider: string, modelId: string): readonl const cached = _unsupportedParamsMap.get(modelId); if (cached) return cached; - // 3. Handle prefixed model IDs (e.g., "openai/o3" → "o3") + // 3. Handle prefixed model IDs (e.g., "openai/o3" → "o3", "moonshotai/Kimi-K2.5" → "moonshotai/Kimi-K2.5") + // ModelScope models have slash in ID, check both full ID and bare ID if (modelId.includes("/")) { + // First check full model ID with provider prefix (e.g., "moonshotai/Kimi-K2.5") + const cachedWithPrefix = _unsupportedParamsMap.get(modelId); + if (cachedWithPrefix) return cachedWithPrefix; + + // Fall back to bare ID (e.g., "Kimi-K2.5") const bareId = modelId.split("/").pop() || ""; const bare = _unsupportedParamsMap.get(bareId); if (bare) return bare; @@ -2154,6 +2179,7 @@ export function getProviderCategory(provider: string): "oauth" | "apikey" { * Derive the latest opus/sonnet/haiku model IDs from the `claude` registry entry. * Picks the first model whose ID matches each family pattern — registry order * determines precedence, so newer models should be listed first. + * @deprecated This function will be removed in v4.0, please use REGISTRY.claude?.models directly */ export function getClaudeCodeDefaultModels(): { opus: string; diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index b8dc191b7b..b987232735 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -735,18 +735,45 @@ export class AntigravityExecutor extends BaseExecutor { // consuming the stream. The client receives the unmodified SSE data. if (response.body) { let sseBuffer = ""; + const decoder = new TextDecoder(); // Singleton for correct streaming decode + const MAX_BUFFER_SIZE = 16 * 1024; // Limit to prevent OOM on large streams + const passThrough = new TransformStream({ transform(chunk, controller) { controller.enqueue(chunk); // Accumulate text to scan for remainingCredits try { - const text = new TextDecoder().decode(chunk, { stream: true }); + const text = decoder.decode(chunk, { stream: true }); sseBuffer += text; + // Limit buffer size to prevent unbounded growth + // Truncate only after a complete newline to avoid splitting SSE lines mid-payload + if (sseBuffer.length > MAX_BUFFER_SIZE) { + const lastNewline = sseBuffer.lastIndexOf( + "\n", + sseBuffer.length - MAX_BUFFER_SIZE + ); + if (lastNewline !== -1) { + sseBuffer = sseBuffer.slice(lastNewline + 1); + } else { + // No newline found in discard region — buffer contains an incomplete SSE line. + // Discard it entirely to avoid returning malformed data; the remainingCredits + // parser won't find valid data in a truncated line anyway. + sseBuffer = ""; + } + } } catch { /* decoding best-effort */ } }, flush() { + // Final decode for any remaining bytes + try { + const text = decoder.decode(); // Flush pending bytes + sseBuffer += text; + } catch { + /* decoding best-effort */ + } + // Parse the accumulated SSE data for remainingCredits try { const lines = sseBuffer.split("\n"); diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 899c4eb00d..fcbcbab1cd 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -271,6 +271,8 @@ function convertSystemToDeveloperRole(body: Record): void { * server-generated prefix (rs_, fc_, resp_, msg_) — so the content is * preserved but the backend won't try to look it up * 4. Always deletes previous_response_id (endpoint doesn't persist responses) + * + * @deprecated This function will be removed in v4.0, Codex executor has updated processing logic */ function stripStoredItemReferences(body: Record): void { // Always strip previous_response_id — the /codex/responses endpoint does not diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 49aefdbc8c..11dc1b1865 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -14,8 +14,6 @@ import { createRequestLogger } from "../utils/requestLogger.ts"; import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts"; import { resolveModelAlias } from "../services/modelDeprecation.ts"; import { getUnsupportedParams } from "../config/providerRegistry.ts"; -import { hasPerModelQuota, lockModelIfPerModelQuota } from "../services/accountFallback.ts"; -import { COOLDOWN_MS } from "../config/constants.ts"; import { buildErrorBody, createErrorResult, @@ -2075,61 +2073,6 @@ export async function handleChatCore({ console.warn( `[provider] Node ${connectionId} account deactivated (${statusCode}) — disabling permanently` ); - } else if (errorType === PROVIDER_ERROR_TYPES.RATE_LIMITED) { - // For providers with per-model quotas (passthrough providers, Gemini), - // each model has independent quota. A 429 on one model must NOT lock out - // the entire connection — other models may still have quota available. - const rateLimitCooldownMs = retryAfterMs || COOLDOWN_MS.rateLimit; - if ( - lockModelIfPerModelQuota( - provider, - connectionId, - model, - "rate_limited", - rateLimitCooldownMs - ) - ) { - console.warn( - `[provider] Node ${connectionId} model-only rate limited (${statusCode}) for ${model} - ${Math.ceil(rateLimitCooldownMs / 1000)}s (connection stays active)` - ); - } else { - const rateLimitedUntil = new Date(Date.now() + rateLimitCooldownMs).toISOString(); - await updateProviderConnection(connectionId, { - rateLimitedUntil: rateLimitedUntil, - testStatus: "unavailable", - lastErrorType: errorType, - lastError: message, - errorCode: statusCode, - healthCheckInterval: null, - lastHealthCheckAt: null, - }); - console.warn( - `[provider] Node ${connectionId} rate limited (${statusCode}) - Next available at ${rateLimitedUntil}` - ); - } - } else if (errorType === PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED) { - // Providers with per-model quotas — lock the model only, not the connection - if ( - lockModelIfPerModelQuota( - provider, - connectionId, - model, - "quota_exhausted", - retryAfterMs || COOLDOWN_MS.rateLimit - ) - ) { - console.warn( - `[provider] Node ${connectionId} model-only quota exhausted (${statusCode}) for ${model} - ${Math.ceil((retryAfterMs || COOLDOWN_MS.rateLimit) / 1000)}s (connection stays active)` - ); - } else { - await updateProviderConnection(connectionId, { - testStatus: "credits_exhausted", - lastErrorType: errorType, - lastError: message, - errorCode: statusCode, - }); - console.warn(`[provider] Node ${connectionId} exhausted quota (${statusCode})`); - } } else if (errorType === PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED) { await updateProviderConnection(connectionId, { isActive: false, diff --git a/open-sse/index.ts b/open-sse/index.ts index fffb5ea17e..2c49410688 100644 --- a/open-sse/index.ts +++ b/open-sse/index.ts @@ -50,6 +50,9 @@ export { isAccountUnavailable, getUnavailableUntil, filterAvailableAccounts, + isProviderInCooldown, + getProviderCooldownRemainingMs, + getProvidersInCooldown, } from "./services/accountFallback.ts"; export { diff --git a/open-sse/mcp-server/tools/advancedTools.ts b/open-sse/mcp-server/tools/advancedTools.ts index 271ee70e8e..655d22bf98 100644 --- a/open-sse/mcp-server/tools/advancedTools.ts +++ b/open-sse/mcp-server/tools/advancedTools.ts @@ -112,97 +112,121 @@ interface BudgetGuardState { let activeBudgetGuard: BudgetGuardState | null = null; type ResilienceProfileConfig = { - profiles: { + requestQueue: { + requestsPerMinute: number; + minTimeBetweenRequestsMs: number; + concurrentRequests: number; + }; + connectionCooldown: { oauth: { - transientCooldown: number; - rateLimitCooldown: number; - maxBackoffLevel: number; - circuitBreakerThreshold: number; - circuitBreakerReset: number; + baseCooldownMs: number; + useUpstreamRetryHints: boolean; + maxBackoffSteps: number; }; apikey: { - transientCooldown: number; - rateLimitCooldown: number; - maxBackoffLevel: number; - circuitBreakerThreshold: number; - circuitBreakerReset: number; + baseCooldownMs: number; + useUpstreamRetryHints: boolean; + maxBackoffSteps: number; }; }; - defaults: { - requestsPerMinute: number; - minTimeBetweenRequests: number; - concurrentRequests: number; + providerBreaker: { + oauth: { + failureThreshold: number; + resetTimeoutMs: number; + }; + apikey: { + failureThreshold: number; + resetTimeoutMs: number; + }; }; }; const RESILIENCE_PROFILES = { aggressive: { - profiles: { + requestQueue: { + requestsPerMinute: 180, + minTimeBetweenRequestsMs: 100, + concurrentRequests: 16, + }, + connectionCooldown: { oauth: { - transientCooldown: 3000, - rateLimitCooldown: 30000, - maxBackoffLevel: 4, - circuitBreakerThreshold: 2, - circuitBreakerReset: 30000, + baseCooldownMs: 30000, + useUpstreamRetryHints: false, + maxBackoffSteps: 4, }, apikey: { - transientCooldown: 2000, - rateLimitCooldown: 0, - maxBackoffLevel: 3, - circuitBreakerThreshold: 3, - circuitBreakerReset: 15000, + baseCooldownMs: 2000, + useUpstreamRetryHints: true, + maxBackoffSteps: 3, }, }, - defaults: { - requestsPerMinute: 180, - minTimeBetweenRequests: 100, - concurrentRequests: 16, + providerBreaker: { + oauth: { + failureThreshold: 2, + resetTimeoutMs: 30000, + }, + apikey: { + failureThreshold: 3, + resetTimeoutMs: 15000, + }, }, }, balanced: { - profiles: { + requestQueue: { + requestsPerMinute: 100, + minTimeBetweenRequestsMs: 200, + concurrentRequests: 10, + }, + connectionCooldown: { oauth: { - transientCooldown: 5000, - rateLimitCooldown: 60000, - maxBackoffLevel: 8, - circuitBreakerThreshold: 3, - circuitBreakerReset: 60000, + baseCooldownMs: 60000, + useUpstreamRetryHints: false, + maxBackoffSteps: 8, }, apikey: { - transientCooldown: 3000, - rateLimitCooldown: 0, - maxBackoffLevel: 5, - circuitBreakerThreshold: 5, - circuitBreakerReset: 30000, + baseCooldownMs: 3000, + useUpstreamRetryHints: true, + maxBackoffSteps: 5, }, }, - defaults: { - requestsPerMinute: 100, - minTimeBetweenRequests: 200, - concurrentRequests: 10, + providerBreaker: { + oauth: { + failureThreshold: 3, + resetTimeoutMs: 60000, + }, + apikey: { + failureThreshold: 5, + resetTimeoutMs: 30000, + }, }, }, conservative: { - profiles: { + requestQueue: { + requestsPerMinute: 60, + minTimeBetweenRequestsMs: 350, + concurrentRequests: 6, + }, + connectionCooldown: { oauth: { - transientCooldown: 8000, - rateLimitCooldown: 120000, - maxBackoffLevel: 10, - circuitBreakerThreshold: 8, - circuitBreakerReset: 120000, + baseCooldownMs: 120000, + useUpstreamRetryHints: false, + maxBackoffSteps: 10, }, apikey: { - transientCooldown: 5000, - rateLimitCooldown: 30000, - maxBackoffLevel: 8, - circuitBreakerThreshold: 8, - circuitBreakerReset: 60000, + baseCooldownMs: 30000, + useUpstreamRetryHints: false, + maxBackoffSteps: 8, }, }, - defaults: { - requestsPerMinute: 60, - minTimeBetweenRequests: 350, - concurrentRequests: 6, + providerBreaker: { + oauth: { + failureThreshold: 8, + resetTimeoutMs: 120000, + }, + apikey: { + failureThreshold: 8, + resetTimeoutMs: 60000, + }, }, }, } satisfies Record<"aggressive" | "balanced" | "conservative", ResilienceProfileConfig>; @@ -460,13 +484,10 @@ export async function handleSetResilienceProfile(args: { }; } - // Apply to OmniRoute via API (contract: PATCH + { profiles, defaults }) + // Apply to OmniRoute via API using the plan-aligned resilience structure. await apiFetch("/api/resilience", { method: "PATCH", - body: JSON.stringify({ - profiles: settings.profiles, - defaults: settings.defaults, - }), + body: JSON.stringify(settings), }); const result = { applied: true, profile: args.profile, settings }; diff --git a/open-sse/package.json b/open-sse/package.json index fae8803126..3768c51670 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -1,6 +1,6 @@ { "name": "@omniroute/open-sse", - "version": "3.6.9", + "version": "3.7.0", "description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration", "type": "module", "main": "index.js", diff --git a/open-sse/services/AGENTS.md b/open-sse/services/AGENTS.md index b5af3cbdd1..9dd5092ef7 100644 --- a/open-sse/services/AGENTS.md +++ b/open-sse/services/AGENTS.md @@ -8,9 +8,9 @@ ### Combo Routing Engine -- **`combo.ts`** (800 LOC) — Entry point for multi-model routing. **`handleComboChat()`** iterates through targets in order until success or all fail. **`resolveComboTargets()`** expands combo config into ordered `ResolvedComboTarget[]` (provider + model + account + credentials). Enforces per-target circuit breaker and fallback logic. +- **`combo.ts`** (800 LOC) — Entry point for multi-model routing. **`handleComboChat()`** iterates through targets in order until success or all fail. **`resolveComboTargets()`** expands combo config into ordered `ResolvedComboTarget[]` (provider + model + account + credentials). Enforces target retry, round-robin slot control, and provider-level resilience gates. - **Strategies** (13 total): `priority` (ordered list), `weighted` (probabilistic), `fill-first` (fill quota first), `round-robin`, `P2C` (power of two choices), `random`, `least-used`, `cost-optimized`, `strict-random`, `auto`, `lkgp` (last known good provider), `context-optimized`, `context-relay`. -- **Circuit Breaker**: Per target, tracks consecutive failures; breaks after threshold, reopens on success or timeout. +- **Provider Breaker Integration**: Combo targets respect the global provider circuit breaker and skip to the next target when a provider is already open. ### Quota & Rate Limiting @@ -120,7 +120,7 @@ Each service requires unit and integration tests. For authoritative coverage req - **Combo-first design**: All routing decisions go through combo engine; fallback strategies are combo targets, not ad-hoc logic - **Service composition**: Small focused modules; combo.ts orchestrates them, not monolithic routing -- **Circuit breaker per-target**: Failures isolated to specific provider+account combo; other targets unaffected +- **Provider breaker is global**: Combo targets respect the shared provider circuit breaker; combo does not maintain a second target-local breaker - **Caching everywhere**: Models, providers, quotas, family fallbacks all pre-cached; invalidated on write - **13 strategies** over hardcoded logic: Strategy pattern allows new routing logic without touching combo.ts core @@ -130,6 +130,6 @@ Each service requires unit and integration tests. For authoritative coverage req - New services must not add blocking I/O to routing hot path - Combo target resolution under 10ms (measure with benchmarks) -- Circuit breaker state per-target, not global +- Combo should not reintroduce a second breaker layer on top of the global provider breaker - All fallback chains tested (no infinite loops) - Coverage requirements: See [`CONTRIBUTING.md#running-tests`](../../CONTRIBUTING.md#running-tests) (60% gate enforced in CI) diff --git a/open-sse/services/accountFallback.ts b/open-sse/services/accountFallback.ts index 95bdf510df..d14a3122f0 100644 --- a/open-sse/services/accountFallback.ts +++ b/open-sse/services/accountFallback.ts @@ -4,11 +4,30 @@ import { BACKOFF_STEPS_MS, RateLimitReason, HTTP_STATUS, - PROVIDER_PROFILES, } from "../config/constants.ts"; import { getPassthroughProviders, getProviderCategory } from "../config/providerRegistry.ts"; +import { + DEFAULT_RESILIENCE_SETTINGS, + resolveResilienceSettings, +} from "../../src/lib/resilience/settings"; +import { + getAllCircuitBreakerStatuses, + getCircuitBreaker, + STATE, +} from "../../src/shared/utils/circuitBreaker"; -type ProviderProfile = (typeof PROVIDER_PROFILES)["oauth"]; +type ProviderProfile = { + baseCooldownMs: number; + useUpstreamRetryHints: boolean; + maxBackoffSteps: number; + failureThreshold: number; + resetTimeoutMs: number; + transientCooldown: number; + rateLimitCooldown: number; + maxBackoffLevel: number; + circuitBreakerThreshold: number; + circuitBreakerReset: number; +}; type JsonRecord = Record; type ModelLockoutEntry = { reason: string; @@ -24,6 +43,11 @@ type ModelFailureState = { resetAfterMs: number; }; +// Error codes that count toward provider-level failure threshold. +// Connection-scoped 429 rate limits stay in connection cooldown handling and +// do not contribute to the shared provider breaker. +const PROVIDER_FAILURE_ERROR_CODES = new Set([408, 500, 502, 503, 504]); + // T06 (sub2api PR #1037): Signals that indicate permanent account deactivation. // When a 401 body contains these strings, the account is permanently dead // and should NOT be retried after token refresh. @@ -82,8 +106,12 @@ const CONTEXT_OVERFLOW_PATTERNS = [ const MALFORMED_REQUEST_PATTERNS = [ /\bimproperly formed request\b/i, /\binvalid.*message.*format/i, - /\bmessages must alternate/i, - /\bempty (message|content)/i, + /\bmessages must alternate\b/i, + /\bempty (message|content)\b/i, + // Tool call function name errors + /\bfunction'?s? name (?:can't|can not|is|has) (?:blank|empty|missing)/i, + /function.*name.*(?:blank|empty|missing)/i, + /tool_call.*name.*(?:blank|empty|missing)/i, ]; /** @@ -111,17 +139,7 @@ export function isOAuthInvalidToken(errorText: string): boolean { return OAUTH_INVALID_TOKEN_SIGNALS.some((sig) => lower.includes(sig)); } -// ─── Provider Profile Helper ──────────────────────────────────────────────── - -/** - * Get the resilience profile for a provider (oauth or apikey). - * @param {string} provider - Provider ID or alias - * @returns {import('../config/constants.js').PROVIDER_PROFILES['oauth']} - */ -export function getProviderProfile(provider) { - const category = getProviderCategory(provider); - return PROVIDER_PROFILES[category] ?? PROVIDER_PROFILES.apikey; -} +// ─── Resilience Profile Helper ────────────────────────────────────────────── function asRecord(value: unknown): JsonRecord { return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; @@ -134,42 +152,47 @@ function isCompatibleProvider(provider: string | null | undefined): boolean { ); } -function mergeProviderProfile(fallback: ProviderProfile, overrides: unknown): ProviderProfile { - const record = asRecord(overrides); +function buildProviderProfile( + category: "oauth" | "apikey", + settings?: Record | null +) { + const resilience = settings ? resolveResilienceSettings(settings) : DEFAULT_RESILIENCE_SETTINGS; + const connectionCooldown = resilience.connectionCooldown[category]; + const providerBreaker = resilience.providerBreaker[category]; + return { - transientCooldown: - typeof record.transientCooldown === "number" - ? record.transientCooldown - : fallback.transientCooldown, - rateLimitCooldown: - typeof record.rateLimitCooldown === "number" - ? record.rateLimitCooldown - : fallback.rateLimitCooldown, - maxBackoffLevel: - typeof record.maxBackoffLevel === "number" - ? record.maxBackoffLevel - : fallback.maxBackoffLevel, - circuitBreakerThreshold: - typeof record.circuitBreakerThreshold === "number" - ? record.circuitBreakerThreshold - : fallback.circuitBreakerThreshold, - circuitBreakerReset: - typeof record.circuitBreakerReset === "number" - ? record.circuitBreakerReset - : fallback.circuitBreakerReset, - }; + baseCooldownMs: connectionCooldown.baseCooldownMs, + useUpstreamRetryHints: connectionCooldown.useUpstreamRetryHints, + maxBackoffSteps: connectionCooldown.maxBackoffSteps, + failureThreshold: providerBreaker.failureThreshold, + resetTimeoutMs: providerBreaker.resetTimeoutMs, + transientCooldown: connectionCooldown.baseCooldownMs, + rateLimitCooldown: connectionCooldown.useUpstreamRetryHints + ? 0 + : connectionCooldown.baseCooldownMs, + maxBackoffLevel: connectionCooldown.maxBackoffSteps, + circuitBreakerThreshold: providerBreaker.failureThreshold, + circuitBreakerReset: providerBreaker.resetTimeoutMs, + } satisfies ProviderProfile; +} + +/** + * Get the resilience profile for a provider (oauth or apikey). + * @param {string} provider - Provider ID or alias + */ +export function getProviderProfile(provider) { + const category = getProviderCategory(provider); + return buildProviderProfile(category); } export async function getRuntimeProviderProfile(provider: string | null | undefined) { - const fallback = getProviderProfile(provider); try { const { getCachedSettings } = await import("@/lib/db/readCache"); const settings = await getCachedSettings(); - const profiles = asRecord(settings.providerProfiles); const category = getProviderCategory(provider); - return mergeProviderProfile(fallback, profiles[category]); + return buildProviderProfile(category, settings); } catch { - return fallback; + return getProviderProfile(provider); } } @@ -183,7 +206,7 @@ function getModelLockKey(provider: string, connectionId: string, model: string) } function getFailureWindowMs(profile: ProviderProfile | null = null, fallbackMs = 30 * 60 * 1000) { - const configured = profile?.circuitBreakerReset; + const configured = profile?.resetTimeoutMs; return typeof configured === "number" && configured > 0 ? configured : fallbackMs; } @@ -205,23 +228,13 @@ function getModelLockBaseCooldown( fallbackCooldownMs: number, profile: ProviderProfile | null = null ) { - if (status === HTTP_STATUS.RATE_LIMITED) { - if (typeof profile?.rateLimitCooldown === "number" && profile.rateLimitCooldown > 0) { - return profile.rateLimitCooldown; - } - if (Number.isFinite(fallbackCooldownMs) && fallbackCooldownMs > 0) { - return fallbackCooldownMs; - } - return getQuotaCooldown(0); - } - - if (typeof profile?.transientCooldown === "number" && profile.transientCooldown > 0) { - return profile.transientCooldown; - } if (Number.isFinite(fallbackCooldownMs) && fallbackCooldownMs > 0) { return fallbackCooldownMs; } - return COOLDOWN_MS.transientInitial; + if (typeof profile?.baseCooldownMs === "number" && profile.baseCooldownMs >= 0) { + return profile.baseCooldownMs; + } + return status === HTTP_STATUS.RATE_LIMITED ? getQuotaCooldown(0) : COOLDOWN_MS.transientInitial; } function getScaledCooldown( @@ -305,7 +318,8 @@ export function recordModelLockoutFailure( reason: string, status: number, fallbackCooldownMs: number, - profile: ProviderProfile | null = null + profile: ProviderProfile | null = null, + options: { exactCooldownMs?: number | null } = {} ) { ensureCleanupTimer(); const key = getModelLockKey(provider, connectionId, model); @@ -323,11 +337,14 @@ export function recordModelLockoutFailure( }); const baseCooldownMs = getModelLockBaseCooldown(status, fallbackCooldownMs, profile); - const cooldownMs = getScaledCooldown( - baseCooldownMs, - failureCount, - profile?.maxBackoffLevel ?? BACKOFF_CONFIG.maxLevel - ); + const cooldownMs = + typeof options.exactCooldownMs === "number" && options.exactCooldownMs > 0 + ? options.exactCooldownMs + : getScaledCooldown( + baseCooldownMs, + failureCount, + profile?.maxBackoffSteps ?? BACKOFF_CONFIG.maxLevel + ); lockModel(provider, connectionId, model, reason, cooldownMs, { failureCount, @@ -355,11 +372,21 @@ export function clearModelLock(provider, connectionId, model) { * Compatible and passthrough providers multiplex multiple upstream models behind one * connection, so transient 404/429 responses should stay model-scoped instead of * poisoning the whole connection. + * + * @param provider - Provider ID + * @param _model - Model ID (reserved for future use) + * @param connectionPassthroughModels - Optional per-connection override from providerSpecificData. + * When provided, takes precedence over registry/provider-level logic. */ export function hasPerModelQuota( provider: string | null | undefined, - _model: string | null | undefined = null + _model: string | null | undefined = null, + connectionPassthroughModels?: boolean ): boolean { + // Connection-level override takes precedence (e.g., user-configured ModelScope) + if (typeof connectionPassthroughModels === "boolean") { + return connectionPassthroughModels; + } if (!provider) return false; if (provider === "gemini") return true; if (getPassthroughProviders().has(provider)) return true; @@ -376,18 +403,23 @@ export function lockModelIfPerModelQuota( connectionId: string, model: string | null, reason: string, - cooldownMs: number + cooldownMs: number, + connectionPassthroughModels?: boolean ): boolean { - if (!hasPerModelQuota(provider, model) || !model) return false; + if (!hasPerModelQuota(provider, model, connectionPassthroughModels) || !model) return false; + // Skip model-level lock if the entire provider is in circuit-breaker cooldown. + // The provider cooldown already prevents all requests, so a model lock is redundant. + if (isProviderInCooldown(provider)) return false; lockModel(provider, connectionId, model, reason, cooldownMs); return true; } export function shouldMarkAccountExhaustedFrom429( provider: string | null | undefined, - model: string | null | undefined = null + model: string | null | undefined = null, + connectionPassthroughModels?: boolean ): boolean { - return !hasPerModelQuota(provider, model); + return !hasPerModelQuota(provider, model, connectionPassthroughModels); } /** @@ -442,6 +474,89 @@ export function getAllModelLockouts() { return active; } +// ─── Provider Breaker Compatibility Wrappers ──────────────────────────────── +// Legacy helpers now delegate to the shared provider circuit breaker. + +function getProviderBreaker(provider: string | null | undefined) { + if (!provider) return null; + const profile = getProviderProfile(provider); + return getCircuitBreaker(provider, { + failureThreshold: profile.failureThreshold ?? profile.circuitBreakerThreshold, + resetTimeout: profile.resetTimeoutMs ?? profile.circuitBreakerReset, + }); +} + +/** + * Check if a provider is currently blocked by the shared circuit breaker. + */ +export function isProviderInCooldown(provider: string | null | undefined): boolean { + const breaker = getProviderBreaker(provider); + return breaker ? !breaker.canExecute() : false; +} + +/** + * Get remaining retry-after time for a provider breaker. + */ +export function getProviderCooldownRemainingMs(provider: string | null | undefined): number | null { + const breaker = getProviderBreaker(provider); + if (!breaker || breaker.canExecute()) return null; + const remaining = breaker.getRetryAfterMs(); + return remaining > 0 ? remaining : null; +} + +/** + * Record a provider failure against the shared circuit breaker. + */ +export function recordProviderFailure( + provider: string | null | undefined, + log?: { warn?: (...args: unknown[]) => void } +): void { + const breaker = getProviderBreaker(provider); + if (!breaker || !provider) return; + breaker._onFailure(); + const status = breaker.getStatus(); + if (status.state === STATE.OPEN) { + log?.warn?.(`[ProviderBreaker] ${provider}: OPEN after ${status.failureCount} final failures`); + } +} + +/** + * Reset the shared provider breaker. + */ +export function clearProviderFailure(provider: string | null | undefined): void { + const breaker = getProviderBreaker(provider); + breaker?.reset(); +} + +/** + * Get all providers currently blocked by the shared breaker. + */ +export function getProvidersInCooldown(): Array<{ + provider: string; + failureCount: number; + cooldownRemainingMs: number | null; + lastFailureAt: number; +}> { + return getAllCircuitBreakerStatuses() + .filter((status) => { + const breaker = getProviderBreaker(status.name); + return Boolean(breaker && !breaker.canExecute()); + }) + .map((status) => ({ + provider: status.name, + failureCount: status.failureCount, + cooldownRemainingMs: status.retryAfterMs || null, + lastFailureAt: status.lastFailureTime, + })); +} + +/** + * Check if a status code should be counted toward provider failure threshold + */ +export function isProviderFailureCode(status: number): boolean { + return PROVIDER_FAILURE_ERROR_CODES.has(status); +} + // ─── Retry-After Parsing ──────────────────────────────────────────────────── /** @@ -625,6 +740,40 @@ export function classifyError(status, errorText) { return RateLimitReason.UNKNOWN; } +// ─── Daily Quota Helpers ──────────────────────────────────────────────────── + +/** + * Calculate milliseconds from now until tomorrow at midnight (00:00:00). + * Used to lock a model until the next day when daily quota is exhausted. + * @returns {number} Milliseconds until tomorrow + */ +export function getMsUntilTomorrow(): number { + const now = new Date(); + const tomorrow = new Date(now); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(0, 0, 0, 0); + const ms = tomorrow.getTime() - now.getTime(); + // Guard against DST edge cases: if ms is negative (shouldn't happen) or + // unreasonably large (>25h due to spring-forward), cap at 24 hours. + return ms > 0 && ms <= 25 * 60 * 60 * 1000 ? ms : 24 * 60 * 60 * 1000; +} + +/** + * Check if error text indicates daily quota exhaustion (as opposed to rate limiting). + * Daily quota errors typically mention "today's quota" or "try again tomorrow". + * @param {string} errorText - Error message text + * @returns {boolean} True if daily quota is exhausted + */ +export function isDailyQuotaExhausted(errorText: string): boolean { + if (!errorText) return false; + const lower = errorText.toLowerCase(); + return ( + lower.includes("today's quota") || + lower.includes("daily quota") || + lower.includes("try again tomorrow") + ); +} + // ─── Configurable Backoff ─────────────────────────────────────────────────── /** @@ -650,16 +799,6 @@ export function getQuotaCooldown(backoffLevel = 0) { return Math.min(cooldown, BACKOFF_CONFIG.max); } -function getRateLimitCooldown(backoffLevel = 0, profile: ProviderProfile | null = null) { - const maxLevel = profile?.maxBackoffLevel ?? BACKOFF_CONFIG.maxLevel; - const cappedLevel = Math.min(Math.max(0, backoffLevel), maxLevel); - const configuredBase = profile?.rateLimitCooldown; - if (typeof configuredBase === "number" && configuredBase > 0) { - return configuredBase * Math.pow(2, cappedLevel); - } - return getQuotaCooldown(cappedLevel); -} - /** * Check if error should trigger account fallback (switch to next account) * @param {number} status - HTTP status code @@ -673,15 +812,24 @@ export function checkFallbackError( status, errorText, backoffLevel = 0, - model = null, + _model = null, provider = null, headers = null, profileOverride: ProviderProfile | null = null ) { const errorStr = (errorText || "").toString(); const profile = profileOverride ?? (provider ? getProviderProfile(provider) : null); + const maxBackoffSteps = profile?.maxBackoffSteps ?? BACKOFF_CONFIG.maxLevel; + const retryableStatuses = new Set([ + HTTP_STATUS.REQUEST_TIMEOUT, + HTTP_STATUS.RATE_LIMITED, + HTTP_STATUS.SERVER_ERROR, + HTTP_STATUS.BAD_GATEWAY, + HTTP_STATUS.SERVICE_UNAVAILABLE, + HTTP_STATUS.GATEWAY_TIMEOUT, + ]); - function parseResetFromHeaders(headers, errorStr = "") { + function parseResetFromHeaders(headers) { if (!headers) return null; // Retry-After header @@ -713,6 +861,59 @@ export function checkFallbackError( } return null; } + + function getUpstreamRetryHintMs() { + if (!profile?.useUpstreamRetryHints) return null; + const resetTime = parseResetFromHeaders(headers); + if (resetTime) { + const waitMs = Math.max(resetTime - Date.now(), 0); + if (waitMs > 0) return waitMs; + } + + const retryFromErrorText = parseRetryFromErrorText(errorStr); + if (retryFromErrorText && retryFromErrorText > 0) { + return retryFromErrorText; + } + + return null; + } + + function getScaledBaseCooldown(reason, level = backoffLevel) { + const baseCooldownMs = + typeof profile?.baseCooldownMs === "number" && profile.baseCooldownMs >= 0 + ? profile.baseCooldownMs + : COOLDOWN_MS.transientInitial; + return { + baseCooldownMs, + cooldownMs: getScaledCooldown(baseCooldownMs, level + 1, maxBackoffSteps), + newBackoffLevel: Math.min(level + 1, maxBackoffSteps), + }; + } + + function buildRetryableFallback(reason) { + const upstreamRetryHintMs = getUpstreamRetryHintMs(); + if (typeof upstreamRetryHintMs === "number" && upstreamRetryHintMs > 0) { + return { + shouldFallback: true, + cooldownMs: upstreamRetryHintMs, + baseCooldownMs: upstreamRetryHintMs, + newBackoffLevel: 0, + usedUpstreamRetryHint: true, + reason, + }; + } + + const scaled = getScaledBaseCooldown(reason, backoffLevel); + return { + shouldFallback: true, + cooldownMs: scaled.cooldownMs, + baseCooldownMs: scaled.baseCooldownMs, + newBackoffLevel: scaled.newBackoffLevel, + usedUpstreamRetryHint: false, + reason, + }; + } + // Check error message FIRST - specific patterns take priority over status codes if (errorText) { const lowerError = errorStr.toLowerCase(); @@ -737,6 +938,19 @@ export function checkFallbackError( }; } + // Daily quota exhausted — lock model until tomorrow + if (isDailyQuotaExhausted(errorStr)) { + const msUntilTomorrow = getMsUntilTomorrow(); + // Cap at 24 hours to handle timezone edge cases + const cooldownMs = Math.min(msUntilTomorrow, 24 * 60 * 60 * 1000); + return { + shouldFallback: true, + cooldownMs, + reason: RateLimitReason.QUOTA_EXHAUSTED, + dailyQuotaExhausted: true, + }; + } + if (lowerError.includes("no credentials")) { return { shouldFallback: true, @@ -749,6 +963,7 @@ export function checkFallbackError( return { shouldFallback: true, cooldownMs: COOLDOWN_MS.requestNotAllowed, + baseCooldownMs: COOLDOWN_MS.requestNotAllowed, reason: RateLimitReason.RATE_LIMIT_EXCEEDED, }; } @@ -764,45 +979,16 @@ export function checkFallbackError( lowerError.includes("capacity") || lowerError.includes("overloaded") ) { - const resetTime = parseResetFromHeaders(headers); - if (resetTime) { - const waitMs = resetTime - Date.now(); - if (waitMs > 60_000) { - return { - shouldFallback: true, - cooldownMs: waitMs, - newBackoffLevel: 0, - reason: RateLimitReason.RATE_LIMIT_EXCEEDED, - }; - } - } - const retryFromBody = parseRetryFromErrorText(errorStr); - if (retryFromBody && retryFromBody > 60_000) { - return { - shouldFallback: true, - cooldownMs: retryFromBody, - newBackoffLevel: 0, - reason: RateLimitReason.RATE_LIMIT_EXCEEDED, - }; - } - const newLevel = Math.min( - backoffLevel + 1, - profile?.maxBackoffLevel ?? BACKOFF_CONFIG.maxLevel - ); const reason = classifyErrorText(errorStr); - return { - shouldFallback: true, - cooldownMs: getRateLimitCooldown(backoffLevel, profile), - newBackoffLevel: newLevel, - reason, - }; + return buildRetryableFallback(reason); } } if (status === HTTP_STATUS.UNAUTHORIZED) { return { shouldFallback: true, - cooldownMs: COOLDOWN_MS.unauthorized, + cooldownMs: 0, + baseCooldownMs: 0, reason: RateLimitReason.AUTH_ERROR, }; } @@ -810,7 +996,8 @@ export function checkFallbackError( if (status === HTTP_STATUS.PAYMENT_REQUIRED || status === HTTP_STATUS.FORBIDDEN) { return { shouldFallback: true, - cooldownMs: COOLDOWN_MS.paymentRequired, + cooldownMs: 0, + baseCooldownMs: 0, reason: RateLimitReason.QUOTA_EXHAUSTED, }; } @@ -825,64 +1012,11 @@ export function checkFallbackError( // 429 - Rate limit with exponential backoff if (status === HTTP_STATUS.RATE_LIMITED) { - const resetTime = parseResetFromHeaders(headers); - if (resetTime) { - const waitMs = resetTime - Date.now(); - if (waitMs > 60_000) { - return { - shouldFallback: true, - cooldownMs: waitMs, - newBackoffLevel: 0, - reason: RateLimitReason.RATE_LIMIT_EXCEEDED, - }; - } - } - - const newLevel = Math.min( - backoffLevel + 1, - profile?.maxBackoffLevel ?? BACKOFF_CONFIG.maxLevel - ); - return { - shouldFallback: true, - cooldownMs: getRateLimitCooldown(backoffLevel, profile), - newBackoffLevel: newLevel, - reason: RateLimitReason.RATE_LIMIT_EXCEEDED, - }; + return buildRetryableFallback(RateLimitReason.RATE_LIMIT_EXCEEDED); } - // Transient / server errors — exponential backoff with provider profile - const transientStatuses = [ - HTTP_STATUS.NOT_ACCEPTABLE, - HTTP_STATUS.REQUEST_TIMEOUT, - HTTP_STATUS.SERVER_ERROR, - HTTP_STATUS.BAD_GATEWAY, - HTTP_STATUS.SERVICE_UNAVAILABLE, - HTTP_STATUS.GATEWAY_TIMEOUT, - ]; - if (transientStatuses.includes(status)) { - const resetTime = parseResetFromHeaders(headers, errorStr); - if (resetTime) { - const waitMs = resetTime - Date.now(); - if (waitMs > 60_000) { - return { - shouldFallback: true, - cooldownMs: waitMs, - newBackoffLevel: 0, - reason: RateLimitReason.SERVER_ERROR, - }; - } - } - - const baseCooldown = profile?.transientCooldown ?? COOLDOWN_MS.transientInitial; - const maxLevel = profile?.maxBackoffLevel ?? BACKOFF_CONFIG.maxLevel; - const cooldownMs = Math.min(baseCooldown * Math.pow(2, backoffLevel), COOLDOWN_MS.transientMax); - const newLevel = Math.min(backoffLevel + 1, maxLevel); - return { - shouldFallback: true, - cooldownMs, - newBackoffLevel: newLevel, - reason: RateLimitReason.SERVER_ERROR, - }; + if (status === HTTP_STATUS.NOT_ACCEPTABLE || retryableStatuses.has(status)) { + return buildRetryableFallback(RateLimitReason.SERVER_ERROR); } // 400 — context overflow / malformed request may succeed on another model in the combo @@ -905,7 +1039,8 @@ export function checkFallbackError( // All other errors - fallback with transient cooldown return { shouldFallback: true, - cooldownMs: COOLDOWN_MS.transient, + cooldownMs: profile?.baseCooldownMs ?? COOLDOWN_MS.transient, + baseCooldownMs: profile?.baseCooldownMs ?? COOLDOWN_MS.transient, reason: RateLimitReason.UNKNOWN, }; } @@ -997,13 +1132,10 @@ export function applyErrorState(account, status, errorText, provider = null) { if (!account) return account; const backoffLevel = account.backoffLevel || 0; - const { cooldownMs, newBackoffLevel, reason } = checkFallbackError( - status, - errorText, - backoffLevel, - null, - provider - ); + const fallbackDecision = checkFallbackError(status, errorText, backoffLevel, null, provider); + const { cooldownMs, reason } = fallbackDecision; + const newBackoffLevel = + "newBackoffLevel" in fallbackDecision ? fallbackDecision.newBackoffLevel : undefined; return { ...account, diff --git a/open-sse/services/cloudCodeThinking.ts b/open-sse/services/cloudCodeThinking.ts index 769c414d7b..9d3b3b944a 100644 --- a/open-sse/services/cloudCodeThinking.ts +++ b/open-sse/services/cloudCodeThinking.ts @@ -23,6 +23,9 @@ function stripGeminiThinkingConfig(value: unknown): unknown { return next; } +/** + * @deprecated This function will be removed in v4.0, reasoning configuration processing has migrated to translateRequest + */ export function shouldStripCloudCodeThinking(provider: string, model: string): boolean { if (!provider || !model) return false; const normalizedModel = normalizeCloudCodeModel(model); @@ -33,6 +36,9 @@ export function shouldStripCloudCodeThinking(provider: string, model: string): b return CLOUD_CODE_REASONING_UNSUPPORTED_PATTERNS.some((pattern) => pattern.test(normalizedModel)); } +/** + * @deprecated This function will be removed in v4.0, reasoning configuration processing has migrated to translateRequest + */ export function stripCloudCodeThinkingConfig( body: Record ): Record { diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index dcbd63a457..ea56cb5c60 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -40,6 +40,16 @@ import { getComboStepWeight, normalizeComboStep, } from "../../src/lib/combos/steps.ts"; + +function isProviderBreakerOpenResponse( + result: Response, + errorBody?: { error?: { code?: string | null } } | null +) { + return ( + result.headers.get("x-omniroute-provider-breaker") === "open" || + errorBody?.error?.code === "provider_circuit_open" + ); +} import { getConnectionRoutingTags, matchesRoutingTags, @@ -47,9 +57,8 @@ import { type RoutingTagMatchMode, } from "../../src/domain/tagRouter.ts"; -// Status codes that should mark semaphore + record circuit breaker failures -// 401, 403 added so Auth errors quickly open the circuit breaker to prevent background request leaks -const TRANSIENT_FOR_BREAKER = [401, 403, 429, 500, 502, 503, 504]; +// Status codes that should mark round-robin target semaphores as cooling down. +const TRANSIENT_FOR_SEMAPHORE = [429, 502, 503, 504]; const COMBO_BAD_REQUEST_FALLBACK_PATTERNS = [ /\bprohibited_content\b/i, /request blocked by .*api/i, @@ -59,6 +68,47 @@ const COMBO_BAD_REQUEST_FALLBACK_PATTERNS = [ /unsupported content part type/i, /tool(?:_call|_use)? .* not (?:available|found)/i, /third-party apps/i, + // Context overflow — model-specific, may succeed on a model with larger context window + /context overflow/i, + /context length exceeded/i, + /prompt too large/i, + /token limit/i, + /too many tokens/i, + /exceeds? context/i, + /maximum context/i, + /input too long/i, + /messages? exceed/i, + // Model not supported/found — permanent model-level error, try next combo target + /no provider supported/i, + /model not found/i, + /model not available/i, + /unsupported model/i, + /model.*has no provider/i, + // Function calling format error — model doesn't support this capability + /function\.?arguments.*(must be|should be|必须).*(json|JSON)/i, + /tool.*arguments.*invalid/i, + /function.*parameter.*(invalid|format)/i, + // Input length range error — model-specific context limit + /range of input length/i, + /input length should be/i, + // Transient 400 errors from upstream — should fallback to next combo target + /服务遇到了一点小状况/i, // ModelScope/Qwen transient error + /抱歉.*?敏感内容.*?请检查/i, // ModelScope/Qwen content moderation with context + /内容.*?敏感.*?(?:无法|过滤)/i, // Content sensitivity block + /无法响应.*?请求/i, // "unable to respond to request" + /稍后重试/i, // "retry later" in Chinese + /temporary.*error/i, + /transient.*error/i, + /service.*unavailable/i, + /please.*try.*again/i, + // Rate limit errors — some providers return 400 instead of 429 + /\brate.?-?limit.?(?:exceeded|reached|hit)/i, + /too many requests/i, + /请求过于频繁/i, // Chinese rate limit message + // Tool call function name errors — model-specific, try next combo target + /\bfunction'?s? name (?:can't|can not|is|has) (?:blank|empty|missing)/i, + /function.*name.*(?:blank|empty|missing)/i, + /tool_call.*name.*(?:blank|empty|missing)/i, ]; // Patterns that signal all accounts for a provider are rate-limited / exhausted. @@ -77,6 +127,7 @@ function isAllAccountsRateLimitedResponse( const MAX_COMBO_DEPTH = 3; const MAX_FALLBACK_WAIT_MS = 5000; +const MAX_GLOBAL_ATTEMPTS = 30; function comboModelNotFoundResponse(message: string) { return errorResponse(404, message); @@ -227,10 +278,6 @@ function getTargetProvider(modelStr: string, providerId?: string | null): string return providerId || parsed.provider || parsed.providerAlias || "unknown"; } -function getComboBreakerKey(comboName: string, executionKey: string): string { - return `combo:${comboName}:${executionKey}`; -} - function toRecordedTarget(target: ResolvedComboTarget) { return { executionKey: target.executionKey, @@ -826,9 +873,7 @@ async function buildAutoCandidates(targets, comboName) { ? Math.max(10, historicalStdDev) : Math.max(10, p95LatencyMs * 0.1); - const breakerStateRaw = getCircuitBreaker( - getComboBreakerKey(comboName, target.executionKey) - )?.getStatus?.()?.state; + const breakerStateRaw = getCircuitBreaker(provider)?.getStatus?.()?.state; const circuitBreakerState = breakerStateRaw === "OPEN" || breakerStateRaw === "HALF_OPEN" ? breakerStateRaw : "CLOSED"; @@ -1451,6 +1496,7 @@ export async function handleComboChat({ let earliestRetryAfter = null; let lastStatus = null; const startTime = Date.now(); + let globalAttempts = 0; let fallbackCount = 0; let recordedAttempts = 0; @@ -1459,18 +1505,6 @@ export async function handleComboChat({ const modelStr = target.modelStr; const provider = target.provider; const profile = await getRuntimeProviderProfile(provider); - const breakerKey = getComboBreakerKey(combo.name, target.executionKey); - const breaker = getCircuitBreaker(breakerKey, { - failureThreshold: profile.circuitBreakerThreshold, - resetTimeout: profile.circuitBreakerReset, - }); - - // Skip model if circuit breaker is OPEN - if (!breaker.canExecute()) { - log.info("COMBO", `Skipping ${modelStr}: circuit breaker OPEN for ${provider}`); - if (i > 0) fallbackCount++; - continue; - } // Pre-check: skip models where all accounts are in cooldown if (isModelAvailable) { @@ -1484,6 +1518,14 @@ export async function handleComboChat({ // Retry loop for transient errors for (let retry = 0; retry <= maxRetries; retry++) { + globalAttempts++; + if (globalAttempts > MAX_GLOBAL_ATTEMPTS) { + log.warn( + "COMBO", + `Maximum combo attempts (${MAX_GLOBAL_ATTEMPTS}) exceeded across all targets and fallbacks. Terminating loop to prevent runaway background requests.` + ); + return errorResponse(503, "Maximum combo retry limit reached"); + } if (retry > 0) { log.info( "COMBO", @@ -1507,7 +1549,6 @@ export async function handleComboChat({ "COMBO", `Model ${modelStr} returned 200 but failed quality check: ${quality.reason}` ); - breaker._onFailure(); recordComboRequest(combo.name, modelStr, { success: false, latencyMs: Date.now() - startTime, @@ -1524,7 +1565,6 @@ export async function handleComboChat({ "COMBO", `Model ${modelStr} succeeded (${latencyMs}ms, ${fallbackCount} fallbacks)` ); - breaker._onSuccess(); recordComboRequest(combo.name, modelStr, { success: true, latencyMs, @@ -1593,6 +1633,7 @@ export async function handleComboChat({ // Extract error info from response let errorText = result.statusText || ""; + let errorBody = null; let retryAfter = null; try { const cloned = result.clone(); @@ -1600,7 +1641,7 @@ export async function handleComboChat({ const text = await cloned.text(); if (text) { errorText = text.substring(0, 500); - const errorBody = JSON.parse(text); + errorBody = JSON.parse(text); errorText = errorBody?.error?.message || errorBody?.error || errorBody?.message || errorText; retryAfter = errorBody?.retryAfter || null; @@ -1629,11 +1670,15 @@ export async function handleComboChat({ } } - const isAllAccountsRateLimited = isAllAccountsRateLimitedResponse( - result.status, - result.headers?.get("content-type") ?? null, - errorText - ); + const providerBreakerOpen = isProviderBreakerOpenResponse(result, errorBody); + + if (providerBreakerOpen) { + lastError = errorText || String(result.status); + if (!lastStatus) lastStatus = result.status; + if (i > 0) fallbackCount++; + log.info("COMBO", `Skipping ${modelStr}: provider circuit breaker OPEN for ${provider}`); + break; + } const { shouldFallback, cooldownMs } = checkFallbackError( result.status, @@ -1646,14 +1691,7 @@ export async function handleComboChat({ ); const comboBadRequestFallback = shouldFallbackComboBadRequest(result.status, errorText); - // Record failure in circuit breaker for transient errors - if (TRANSIENT_FOR_BREAKER.includes(result.status)) { - breaker._onFailure(); - } - - if (isAllAccountsRateLimited) { - log.info("COMBO", `All accounts rate-limited for ${modelStr}, falling back to next model`); - } else if (!shouldFallback && !comboBadRequestFallback) { + if (!shouldFallback && !comboBadRequestFallback) { log.warn("COMBO", `Model ${modelStr} failed (no fallback)`, { status: result.status }); recordComboRequest(combo.name, modelStr, { success: false, @@ -1706,25 +1744,12 @@ export async function handleComboChat({ } } - // Early exit: check if all models have breaker OPEN - const allBreakersOpen = orderedTargets.every((target) => { - return !getCircuitBreaker(getComboBreakerKey(combo.name, target.executionKey)).canExecute(); - }); - // All models failed const latencyMs = Date.now() - startTime; if (recordedAttempts === 0) { recordComboRequest(combo.name, null, { success: false, latencyMs, fallbackCount, strategy }); } - if (allBreakersOpen) { - log.warn("COMBO", "All models have circuit breaker OPEN — aborting"); - return unavailableResponse( - 503, - "All providers temporarily unavailable (circuit breakers open)" - ); - } - if (!lastStatus) { return new Response( JSON.stringify({ @@ -1799,6 +1824,7 @@ async function handleRoundRobinCombo({ let lastError = null; let lastStatus = null; let earliestRetryAfter = null; + let globalAttempts = 0; let fallbackCount = 0; let recordedAttempts = 0; @@ -1809,19 +1835,7 @@ async function handleRoundRobinCombo({ const modelStr = target.modelStr; const provider = target.provider; const profile = await getRuntimeProviderProfile(provider); - const breakerKey = getComboBreakerKey(combo.name, target.executionKey); const semaphoreKey = `combo:${combo.name}:${target.executionKey}`; - const breaker = getCircuitBreaker(breakerKey, { - failureThreshold: profile.circuitBreakerThreshold, - resetTimeout: profile.circuitBreakerReset, - }); - - // Skip model if circuit breaker is OPEN - if (!breaker.canExecute()) { - log.info("COMBO-RR", `Skipping ${modelStr}: circuit breaker OPEN for ${provider}`); - if (offset > 0) fallbackCount++; - continue; - } // Pre-check availability if (isModelAvailable) { @@ -1852,6 +1866,14 @@ async function handleRoundRobinCombo({ // Retry loop within this model try { for (let retry = 0; retry <= maxRetries; retry++) { + globalAttempts++; + if (globalAttempts > MAX_GLOBAL_ATTEMPTS) { + log.warn( + "COMBO-RR", + `Maximum combo attempts (${MAX_GLOBAL_ATTEMPTS}) exceeded. Terminating loop to prevent runaway requests.` + ); + return errorResponse(503, "Maximum combo retry limit reached"); + } if (retry > 0) { log.info( "COMBO-RR", @@ -1875,7 +1897,6 @@ async function handleRoundRobinCombo({ "COMBO-RR", `${modelStr} returned 200 but failed quality check: ${quality.reason}` ); - breaker._onFailure(); recordComboRequest(combo.name, modelStr, { success: false, latencyMs: Date.now() - startTime, @@ -1892,7 +1913,6 @@ async function handleRoundRobinCombo({ "COMBO-RR", `${modelStr} succeeded (${latencyMs}ms, ${fallbackCount} fallbacks)` ); - breaker._onSuccess(); recordComboRequest(combo.name, modelStr, { success: true, latencyMs, @@ -1924,13 +1944,14 @@ async function handleRoundRobinCombo({ // Extract error info let errorText = result.statusText || ""; let retryAfter = null; + let errorBody: { error?: { code?: string | null; message?: string | null } } | null = null; try { const cloned = result.clone(); try { const text = await cloned.text(); if (text) { errorText = text.substring(0, 500); - const errorBody = JSON.parse(text); + errorBody = JSON.parse(text); errorText = errorBody?.error?.message || errorBody?.error || errorBody?.message || errorText; retryAfter = errorBody?.retryAfter || null; @@ -1957,6 +1978,17 @@ async function handleRoundRobinCombo({ } } + if (isProviderBreakerOpenResponse(result, errorBody)) { + lastError = errorText || String(result.status); + if (!lastStatus) lastStatus = result.status; + if (offset > 0) fallbackCount++; + log.info( + "COMBO-RR", + `Skipping ${modelStr}: provider circuit breaker OPEN for ${provider}` + ); + break; + } + const { shouldFallback, cooldownMs } = checkFallbackError( result.status, errorText, @@ -1974,14 +2006,10 @@ async function handleRoundRobinCombo({ errorText ); - // Transient errors → mark in semaphore AND record circuit breaker failure - if (TRANSIENT_FOR_BREAKER.includes(result.status) && cooldownMs > 0) { + // Transient errors → mark in semaphore so round-robin stops stampeding this target. + if (TRANSIENT_FOR_SEMAPHORE.includes(result.status) && cooldownMs > 0) { semaphore.markRateLimited(semaphoreKey, cooldownMs); - breaker._onFailure(); - log.warn( - "COMBO-RR", - `${modelStr} error ${result.status}, cooldown ${cooldownMs}ms (breaker: ${breaker.getStatus().failureCount}/${profile.circuitBreakerThreshold})` - ); + log.warn("COMBO-RR", `${modelStr} error ${result.status}, cooldown ${cooldownMs}ms`); } if (isAllAccountsRateLimited) { @@ -2057,19 +2085,6 @@ async function handleRoundRobinCombo({ }); } - // Early exit: check if all models have breaker OPEN - const allBreakersOpen = orderedTargets.every((target) => { - return !getCircuitBreaker(getComboBreakerKey(combo.name, target.executionKey)).canExecute(); - }); - - if (allBreakersOpen) { - log.warn("COMBO-RR", "All models have circuit breaker OPEN — aborting"); - return unavailableResponse( - 503, - "All providers temporarily unavailable (circuit breakers open)" - ); - } - if (!lastStatus) { return new Response( JSON.stringify({ diff --git a/open-sse/services/comboConfig.ts b/open-sse/services/comboConfig.ts index 2b3269fd58..af464f1aa8 100644 --- a/open-sse/services/comboConfig.ts +++ b/open-sse/services/comboConfig.ts @@ -9,11 +9,8 @@ const DEFAULT_COMBO_CONFIG = { strategy: "priority", maxRetries: 1, retryDelayMs: 2000, - timeoutMs: 600000, concurrencyPerModel: 3, // max simultaneous requests per model (round-robin) queueTimeoutMs: 30000, // max wait time in semaphore queue (round-robin) - healthCheckEnabled: true, - healthCheckTimeoutMs: 3000, handoffThreshold: 0.85, handoffModel: "", handoffProviders: ["codex"], @@ -22,6 +19,12 @@ const DEFAULT_COMBO_CONFIG = { trackMetrics: true, }; +const LEGACY_COMBO_RESILIENCE_KEYS = new Set([ + "timeoutMs", + "healthCheckEnabled", + "healthCheckTimeoutMs", +]); + /** * Resolve effective config for a combo, applying cascade: * DEFAULT_COMBO_CONFIG → settings.comboDefaults → settings.providerOverrides[provider] → combo.config @@ -38,7 +41,12 @@ export function resolveComboConfig(combo, settings, provider?: string | null) { // Clean undefined values before spreading const clean = (obj) => - Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== null)); + Object.fromEntries( + Object.entries(obj).filter( + ([key, value]) => + value !== undefined && value !== null && !LEGACY_COMBO_RESILIENCE_KEYS.has(key) + ) + ); return { ...DEFAULT_COMBO_CONFIG, diff --git a/open-sse/services/errorClassifier.ts b/open-sse/services/errorClassifier.ts index 39d513ffbe..4520c2a6e4 100644 --- a/open-sse/services/errorClassifier.ts +++ b/open-sse/services/errorClassifier.ts @@ -1,6 +1,7 @@ import { isAccountDeactivated, isCreditsExhausted, + isDailyQuotaExhausted, isOAuthInvalidToken, } from "./accountFallback.ts"; @@ -100,7 +101,11 @@ export function classifyProviderError(statusCode: number, responseBody: unknown) return PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED; } + // 429: Check if it's a daily quota exhaustion (lock until tomorrow) vs regular rate limit if (statusCode === 429) { + if (isDailyQuotaExhausted(bodyStr)) { + return PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED; + } return PROVIDER_ERROR_TYPES.RATE_LIMITED; } diff --git a/open-sse/services/rateLimitManager.ts b/open-sse/services/rateLimitManager.ts index 35533938de..b4d83961e0 100644 --- a/open-sse/services/rateLimitManager.ts +++ b/open-sse/services/rateLimitManager.ts @@ -9,10 +9,14 @@ */ import Bottleneck from "bottleneck"; -import { parseRetryAfterFromBody, lockModel } from "./accountFallback.ts"; +import { parseRetryAfterFromBody } from "./accountFallback.ts"; import { getProviderCategory } from "../config/providerRegistry.ts"; -import { DEFAULT_API_LIMITS } from "../config/constants.ts"; import { getCodexRateLimitKey } from "../executors/codex.ts"; +import { + DEFAULT_RESILIENCE_SETTINGS, + resolveResilienceSettings, + type RequestQueueSettings, +} from "../../src/lib/resilience/settings"; interface LearnedLimitEntry { provider: string; @@ -24,7 +28,9 @@ interface LearnedLimitEntry { } interface LimiterUpdateSettings { + maxConcurrent?: number | null; minTime: number; + maxWait?: number | null; reservoir?: number | null; reservoirRefreshAmount?: number | null; reservoirRefreshInterval?: number | null; @@ -61,20 +67,18 @@ const PERSIST_DEBOUNCE_MS = 60_000; // Debounce persistence to every 60s max // Track initialization let initialized = false; -// Max time (ms) a job can wait in queue before failing with a timeout error. -// Prevents infinite queuing when all providers are exhausted after a 429. -// Configurable via RATE_LIMIT_MAX_WAIT_MS env var (default: 2 minutes). -const MAX_WAIT_MS = parseInt(process.env.RATE_LIMIT_MAX_WAIT_MS || "120000", 10); +let currentRequestQueueSettings: RequestQueueSettings = DEFAULT_RESILIENCE_SETTINGS.requestQueue; -// Default conservative settings (before we learn from headers) -const DEFAULT_SETTINGS = { - maxConcurrent: 10, - minTime: 0, // No throttle by default — let headers teach us - reservoir: null, // No initial reservoir — unlimited until we learn - reservoirRefreshAmount: null, - reservoirRefreshInterval: null, - maxWait: MAX_WAIT_MS, // Fail-fast: don't queue forever on 429 exhaustion -}; +function buildLimiterDefaults() { + return { + maxConcurrent: currentRequestQueueSettings.concurrentRequests, + minTime: currentRequestQueueSettings.minTimeBetweenRequestsMs, + reservoir: currentRequestQueueSettings.requestsPerMinute, + reservoirRefreshAmount: currentRequestQueueSettings.requestsPerMinute, + reservoirRefreshInterval: 60 * 1000, + maxWait: currentRequestQueueSettings.maxWaitMs, + }; +} function trackAsyncOperation(promise: Promise): Promise { pendingAsyncOperations.add(promise); @@ -93,11 +97,12 @@ export async function initializeRateLimits() { initialized = true; try { - const { getProviderConnections } = await import("@/lib/localDb"); - const connections = await getProviderConnections(); + const { getProviderConnections, getSettings } = await import("@/lib/localDb"); + const [connections, settings] = await Promise.all([getProviderConnections(), getSettings()]); + const resilience = resolveResilienceSettings(settings); + applyRequestQueueSettings(resilience.requestQueue); let explicitCount = 0; let autoCount = 0; - let customCount = 0; for (const connRaw of connections as unknown[]) { const conn = toRecord(connRaw); @@ -105,38 +110,17 @@ export async function initializeRateLimits() { const provider = typeof conn.provider === "string" ? conn.provider : ""; const isActive = conn.isActive === true; const rateLimitProtection = conn.rateLimitProtection === true; - const customRpm = toNumber(conn.customRpm, 0); - const customTpm = toNumber(conn.customTpm, 0); if (!connectionId || !provider) continue; - // Custom rpm/tpm configured — enable rate limiting with user-defined values (#198) - if (customRpm > 0 || customTpm > 0) { - enabledConnections.add(connectionId); - customCount++; - - const key = `${provider}:${connectionId}`; - const rpm = customRpm > 0 ? customRpm : DEFAULT_API_LIMITS.requestsPerMinute; - const minTime = Math.max(0, Math.floor(60000 / rpm) - 10); - - if (!limiters.has(key)) { - limiters.set( - key, - new Bottleneck({ - maxConcurrent: DEFAULT_API_LIMITS.concurrentRequests, - minTime, - reservoir: rpm, - reservoirRefreshAmount: rpm, - reservoirRefreshInterval: 60 * 1000, - maxWait: MAX_WAIT_MS, - id: key, - }) - ); - } - } else if (rateLimitProtection) { + if (rateLimitProtection) { // Explicitly enabled by user enabledConnections.add(connectionId); explicitCount++; - } else if (getProviderCategory(provider) === "apikey" && isActive) { + } else if ( + resilience.requestQueue.autoEnableApiKeyProviders && + getProviderCategory(provider) === "apikey" && + isActive + ) { // Auto-enable for API key providers (safety net) enabledConnections.add(connectionId); autoCount++; @@ -147,12 +131,7 @@ export async function initializeRateLimits() { limiters.set( key, new Bottleneck({ - maxConcurrent: DEFAULT_API_LIMITS.concurrentRequests, - minTime: DEFAULT_API_LIMITS.minTimeBetweenRequests, - reservoir: DEFAULT_API_LIMITS.requestsPerMinute, - reservoirRefreshAmount: DEFAULT_API_LIMITS.requestsPerMinute, - reservoirRefreshInterval: 60 * 1000, // Refresh every minute - maxWait: MAX_WAIT_MS, + ...buildLimiterDefaults(), id: key, }) ); @@ -160,9 +139,9 @@ export async function initializeRateLimits() { } } - if (explicitCount > 0 || autoCount > 0 || customCount > 0) { + if (explicitCount > 0 || autoCount > 0) { console.log( - `🛡️ [RATE-LIMIT] Loaded ${explicitCount} explicit + ${autoCount} auto-enabled + ${customCount} custom rpm/tpm protection(s)` + `🛡️ [RATE-LIMIT] Loaded ${explicitCount} explicit + ${autoCount} auto-enabled protection(s)` ); } @@ -173,6 +152,21 @@ export async function initializeRateLimits() { } } +export function applyRequestQueueSettings(nextSettings: RequestQueueSettings) { + currentRequestQueueSettings = { ...nextSettings }; + + for (const limiter of limiters.values()) { + limiter.updateSettings({ + maxConcurrent: currentRequestQueueSettings.concurrentRequests, + minTime: currentRequestQueueSettings.minTimeBetweenRequestsMs, + maxWait: currentRequestQueueSettings.maxWaitMs, + reservoir: currentRequestQueueSettings.requestsPerMinute, + reservoirRefreshAmount: currentRequestQueueSettings.requestsPerMinute, + reservoirRefreshInterval: 60 * 1000, + }); + } +} + /** * Enable rate limit protection for a connection */ @@ -222,7 +216,7 @@ function getLimiter(provider, connectionId, model = null) { if (!limiters.has(key)) { const limiter = new Bottleneck({ - ...DEFAULT_SETTINGS, + ...buildLimiterDefaults(), id: key, }); @@ -648,10 +642,5 @@ export function updateFromResponseBody(provider, connectionId, responseBody, sta reservoirRefreshAmount: 60, reservoirRefreshInterval: retryAfterMs, }); - - // Also apply model-level lockout if model is known - if (model) { - lockModel(provider, connectionId, model, reason, retryAfterMs); - } } } diff --git a/open-sse/translator/helpers/claudeHelper.ts b/open-sse/translator/helpers/claudeHelper.ts index 2936b60240..bf4e589d0f 100644 --- a/open-sse/translator/helpers/claudeHelper.ts +++ b/open-sse/translator/helpers/claudeHelper.ts @@ -247,10 +247,16 @@ export function prepareClaudeRequest( let hasToolUse = false; let hasThinking = false; - // Always replace signature for all thinking blocks + // Convert thinking blocks to redacted_thinking and replace signature. + // When requests cross provider boundaries (e.g., combo fallback), the + // original thinking signature is invalid for the new provider, causing + // "Invalid signature in thinking block" 400 errors. redacted_thinking + // blocks are accepted without signature validation. for (const block of content) { if (block.type === "thinking" || block.type === "redacted_thinking") { + block.type = "redacted_thinking"; block.signature = DEFAULT_THINKING_CLAUDE_SIGNATURE; + delete block.thinking; hasThinking = true; } if (block.type === "tool_use") hasToolUse = true; diff --git a/open-sse/utils/error.ts b/open-sse/utils/error.ts index 723fff1a84..c59f8c1823 100644 --- a/open-sse/utils/error.ts +++ b/open-sse/utils/error.ts @@ -243,6 +243,33 @@ export function unavailableResponse( }); } +export function providerCircuitOpenResponse( + provider: string, + retryAfter?: string | number | Date | null +) { + const retryAfterSec = normalizeRetryAfterSeconds(retryAfter); + return new Response( + JSON.stringify({ + error: { + message: `Provider ${provider} circuit breaker is open`, + type: "server_error", + code: "provider_circuit_open", + provider, + retry_after: retryAfterSec, + }, + }), + { + status: 503, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": getCorsOrigin(), + "Retry-After": String(retryAfterSec), + "X-OmniRoute-Provider-Breaker": "open", + }, + } + ); +} + export function buildModelCooldownBody({ model, retryAfterSec, diff --git a/package-lock.json b/package-lock.json index bb28534564..5da6f07344 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.6.9", + "version": "3.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.6.9", + "version": "3.7.0", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -20962,7 +20962,7 @@ }, "open-sse": { "name": "@omniroute/open-sse", - "version": "3.6.9" + "version": "3.7.0" } } } diff --git a/package.json b/package.json index 6b2cb413e8..fcc76e04a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.6.9", + "version": "3.7.0", "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": { diff --git a/src/app/(dashboard)/dashboard/combos/IntelligentComboPanel.tsx b/src/app/(dashboard)/dashboard/combos/IntelligentComboPanel.tsx index 0a22d20e07..9a8140d06e 100644 --- a/src/app/(dashboard)/dashboard/combos/IntelligentComboPanel.tsx +++ b/src/app/(dashboard)/dashboard/combos/IntelligentComboPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import Button from "@/shared/components/Button"; import Card from "@/shared/components/Card"; import { useNotificationStore } from "@/store/notificationStore"; @@ -8,7 +8,6 @@ import { FACTOR_LABELS, MODE_PACK_OPTIONS, buildIntelligentProviderScores, - extractIntelligentHealthState, normalizeIntelligentRoutingConfig, } from "@/lib/combos/intelligentRouting"; @@ -46,8 +45,6 @@ export default function IntelligentComboPanel({ onComboUpdated?: (combo: any) => void; }) { const notify = useNotificationStore(); - const [incidentMode, setIncidentMode] = useState(false); - const [exclusions, setExclusions] = useState([]); const [savingModePack, setSavingModePack] = useState(null); const normalizedConfig = useMemo( () => normalizeIntelligentRoutingConfig(combo?.config), @@ -55,35 +52,6 @@ export default function IntelligentComboPanel({ ); const providerScores = useMemo(() => buildIntelligentProviderScores(combo), [combo]); - const fetchHealth = useCallback(async () => { - try { - const response = await fetch("/api/monitoring/health"); - if (!response.ok) { - setIncidentMode(false); - setExclusions([]); - return; - } - - const health = await response.json(); - const nextState = extractIntelligentHealthState(health); - setIncidentMode(nextState.incidentMode); - setExclusions(nextState.exclusions); - } catch { - setIncidentMode(false); - setExclusions([]); - } - }, []); - - useEffect(() => { - const timeoutId = setTimeout(fetchHealth, 0); - const intervalId = setInterval(fetchHealth, 30_000); - - return () => { - clearTimeout(timeoutId); - clearInterval(intervalId); - }; - }, [fetchHealth]); - const handleModePackChange = async (modePackId: string) => { if (!combo?.id || modePackId === normalizedConfig.modePack) return; setSavingModePack(modePackId); @@ -156,19 +124,9 @@ export default function IntelligentComboPanel({ -
- - {incidentMode ? "warning" : "verified"} - - {incidentMode - ? getI18nOrFallback(t, "incidentMode", "Incident Mode") - : getI18nOrFallback(t, "normalOperation", "Normal Operation")} +
+ tune + {getI18nOrFallback(t, "configOnlyStatus", "Configuration View")}
@@ -180,22 +138,20 @@ export default function IntelligentComboPanel({ {getI18nOrFallback(t, "statusOverview", "Status Overview")}

- {incidentMode - ? getI18nOrFallback( - t, - "highCircuitBreakerRate", - "High circuit breaker trip rate detected." - ) - : getI18nOrFallback( - t, - "allProvidersHealthy", - "Providers are reporting healthy routing conditions." - )} + {getI18nOrFallback( + t, + "configOnlyHint", + "This panel shows routing inputs only. Live breaker state is available on the Health page." + )}

-

Exclusions

-

{exclusions.length}

+

+ Candidate Pool +

+

+ {normalizedConfig.candidatePool.length || activeProviders.length} +

@@ -317,51 +273,33 @@ export default function IntelligentComboPanel({

- {getI18nOrFallback(t, "excludedProviders", "Excluded Providers")} + {getI18nOrFallback(t, "routingInputs", "Routing Inputs")}

{getI18nOrFallback( t, - "excludedProvidersHint", - "Providers with an OPEN circuit breaker are temporarily excluded from routing." + "routingInputsHint", + "Mode pack and weighting stay here; breaker runtime state stays on the Health page." )}

-
- {exclusions.length === 0 ? ( -
-
- verified - {getI18nOrFallback( - t, - "noExcludedProviders", - "No providers are currently excluded." - )} -
-
- ) : ( - exclusions.map((exclusion) => ( -
-
-
-

{exclusion.provider}

-

{exclusion.reason}

-
- - {getI18nOrFallback(t, "cooldownMinutes", "Cooldown: {minutes}m").replace( - "{minutes}", - `${Math.ceil(exclusion.cooldownMs / 60000)}` - )} - -
-
- )) - )} +
+
+

Mode Pack

+

+ {normalizedConfig.modePack} +

+
+
+

+ Exploration Rate +

+

+ {Math.round(normalizedConfig.explorationRate * 100)}% +

+
diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index e5932b019a..6406158e7b 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -140,12 +140,26 @@ const STRATEGY_GUIDANCE_FALLBACK = { const ADVANCED_FIELD_HELP_FALLBACK = { maxRetries: "How many retries are attempted before failing the request.", retryDelay: "Initial delay between retries. Higher values reduce burst pressure.", - timeout: "Maximum request time before aborting. Set higher for long generations.", - healthcheck: "Skips unhealthy models/providers from routing decisions when enabled.", concurrencyPerModel: "Max simultaneous requests sent to each model in round-robin.", queueTimeout: "How long a request can wait in queue before timeout in round-robin.", }; +const LEGACY_COMBO_RESILIENCE_KEYS = new Set([ + "timeoutMs", + "healthCheckEnabled", + "healthCheckTimeoutMs", +]); + +function sanitizeComboRuntimeConfig(config) { + if (!config || typeof config !== "object") return {}; + return Object.fromEntries( + Object.entries(config).filter( + ([key, value]) => + value !== undefined && value !== null && !LEGACY_COMBO_RESILIENCE_KEYS.has(key) + ) + ); +} + const STRATEGY_RECOMMENDATIONS_FALLBACK = { priority: { title: "Fail-safe baseline", @@ -334,7 +348,6 @@ const COMBO_TEMPLATES = [ config: { maxRetries: 3, retryDelayMs: 500, - healthCheckEnabled: true, }, }, { @@ -349,7 +362,6 @@ const COMBO_TEMPLATES = [ config: { maxRetries: 2, retryDelayMs: 1500, - healthCheckEnabled: true, }, }, { @@ -364,7 +376,6 @@ const COMBO_TEMPLATES = [ config: { maxRetries: 1, retryDelayMs: 500, - healthCheckEnabled: true, }, }, { @@ -379,7 +390,6 @@ const COMBO_TEMPLATES = [ config: { maxRetries: 1, retryDelayMs: 1000, - healthCheckEnabled: true, }, }, { @@ -394,7 +404,6 @@ const COMBO_TEMPLATES = [ config: { maxRetries: 2, retryDelayMs: 1000, - healthCheckEnabled: true, }, }, ]; @@ -740,7 +749,7 @@ export default function CombosPage() { name: newName, models: combo.models, strategy: combo.strategy || "priority", - config: combo.config || {}, + config: sanitizeComboRuntimeConfig(combo.config), }; await handleCreate(data); @@ -1814,7 +1823,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { const [builderError, setBuilderError] = useState(""); const [builderStage, setBuilderStage] = useState(COMBO_BUILDER_STAGES[0]); const [showAdvanced, setShowAdvanced] = useState(false); - const [config, setConfig] = useState(combo?.config || {}); + const [config, setConfig] = useState(sanitizeComboRuntimeConfig(combo?.config)); const [showStrategyNudge, setShowStrategyNudge] = useState(false); const strategyChangeMountedRef = useRef(false); // Agent features (#399 / #401 / #454) @@ -1840,8 +1849,10 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { } : {}; const nextConfig = nextCombo?.config - ? { ...nextCombo.config } - : Object.fromEntries(Object.entries(nextDefaults).filter(([key]) => key !== "strategy")); + ? sanitizeComboRuntimeConfig(nextCombo.config) + : sanitizeComboRuntimeConfig( + Object.fromEntries(Object.entries(nextDefaults).filter(([key]) => key !== "strategy")) + ); setName(nextCombo?.name || ""); setModels((nextCombo?.models || []).map((m) => normalizeModelEntry(m))); @@ -2317,25 +2328,23 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { const applyStrategyRecommendations = () => { const strategyDefaults = { - priority: { maxRetries: 2, retryDelayMs: 1500, healthCheckEnabled: true }, - weighted: { maxRetries: 1, retryDelayMs: 1000, healthCheckEnabled: true }, + priority: { maxRetries: 2, retryDelayMs: 1500 }, + weighted: { maxRetries: 1, retryDelayMs: 1000 }, "round-robin": { maxRetries: 1, retryDelayMs: 750, - healthCheckEnabled: true, concurrencyPerModel: 3, queueTimeoutMs: 30000, }, "context-relay": { maxRetries: 1, retryDelayMs: 750, - healthCheckEnabled: true, handoffThreshold: 0.85, maxMessagesForSummary: 30, }, - random: { maxRetries: 1, retryDelayMs: 1000, healthCheckEnabled: true }, - "least-used": { maxRetries: 1, retryDelayMs: 1000, healthCheckEnabled: true }, - "cost-optimized": { maxRetries: 1, retryDelayMs: 500, healthCheckEnabled: true }, + random: { maxRetries: 1, retryDelayMs: 1000 }, + "least-used": { maxRetries: 1, retryDelayMs: 1000 }, + "cost-optimized": { maxRetries: 1, retryDelayMs: 500 }, }; const defaults = strategyDefaults[strategy] || strategyDefaults.priority; @@ -2466,7 +2475,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { }; // Include config only if any values are set - const configToSave = { ...config }; + const configToSave = sanitizeComboRuntimeConfig(config); // Add round-robin specific fields to config if (strategy === "round-robin") { if (config.concurrencyPerModel !== undefined) @@ -3225,48 +3234,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { className="w-full text-xs py-1.5 px-2 rounded border border-black/10 dark:border-white/10 bg-transparent focus:border-primary focus:outline-none" /> -
- - - setConfig({ - ...config, - timeoutMs: e.target.value ? Number(e.target.value) : undefined, - }) - } - className="w-full text-xs py-1.5 px-2 rounded border border-black/10 dark:border-white/10 bg-transparent focus:border-primary focus:outline-none" - /> -
-
- - - setConfig({ ...config, healthCheckEnabled: e.target.checked }) - } - className="accent-primary" - /> -
{strategy === "round-robin" && (
diff --git a/src/app/(dashboard)/dashboard/endpoint/components/MCPDashboard.tsx b/src/app/(dashboard)/dashboard/endpoint/components/MCPDashboard.tsx index 2bbd3df717..e4bd6de419 100644 --- a/src/app/(dashboard)/dashboard/endpoint/components/MCPDashboard.tsx +++ b/src/app/(dashboard)/dashboard/endpoint/components/MCPDashboard.tsx @@ -69,72 +69,90 @@ const AUDIT_PAGE_SIZE = 20; const RESILIENCE_PRESETS = { aggressive: { - profiles: { + requestQueue: { + requestsPerMinute: 180, + minTimeBetweenRequestsMs: 100, + concurrentRequests: 16, + }, + connectionCooldown: { oauth: { - transientCooldown: 3000, - rateLimitCooldown: 30000, - maxBackoffLevel: 4, - circuitBreakerThreshold: 2, - circuitBreakerReset: 30000, + baseCooldownMs: 30000, + useUpstreamRetryHints: false, + maxBackoffSteps: 4, }, apikey: { - transientCooldown: 2000, - rateLimitCooldown: 0, - maxBackoffLevel: 3, - circuitBreakerThreshold: 3, - circuitBreakerReset: 15000, + baseCooldownMs: 2000, + useUpstreamRetryHints: true, + maxBackoffSteps: 3, }, }, - defaults: { - requestsPerMinute: 180, - minTimeBetweenRequests: 100, - concurrentRequests: 16, + providerBreaker: { + oauth: { + failureThreshold: 2, + resetTimeoutMs: 30000, + }, + apikey: { + failureThreshold: 3, + resetTimeoutMs: 15000, + }, }, }, balanced: { - profiles: { + requestQueue: { + requestsPerMinute: 100, + minTimeBetweenRequestsMs: 200, + concurrentRequests: 10, + }, + connectionCooldown: { oauth: { - transientCooldown: 5000, - rateLimitCooldown: 60000, - maxBackoffLevel: 8, - circuitBreakerThreshold: 3, - circuitBreakerReset: 60000, + baseCooldownMs: 60000, + useUpstreamRetryHints: false, + maxBackoffSteps: 8, }, apikey: { - transientCooldown: 3000, - rateLimitCooldown: 0, - maxBackoffLevel: 5, - circuitBreakerThreshold: 5, - circuitBreakerReset: 30000, + baseCooldownMs: 3000, + useUpstreamRetryHints: true, + maxBackoffSteps: 5, }, }, - defaults: { - requestsPerMinute: 100, - minTimeBetweenRequests: 200, - concurrentRequests: 10, + providerBreaker: { + oauth: { + failureThreshold: 3, + resetTimeoutMs: 60000, + }, + apikey: { + failureThreshold: 5, + resetTimeoutMs: 30000, + }, }, }, conservative: { - profiles: { + requestQueue: { + requestsPerMinute: 60, + minTimeBetweenRequestsMs: 350, + concurrentRequests: 6, + }, + connectionCooldown: { oauth: { - transientCooldown: 8000, - rateLimitCooldown: 120000, - maxBackoffLevel: 10, - circuitBreakerThreshold: 8, - circuitBreakerReset: 120000, + baseCooldownMs: 120000, + useUpstreamRetryHints: false, + maxBackoffSteps: 10, }, apikey: { - transientCooldown: 5000, - rateLimitCooldown: 30000, - maxBackoffLevel: 8, - circuitBreakerThreshold: 8, - circuitBreakerReset: 60000, + baseCooldownMs: 30000, + useUpstreamRetryHints: false, + maxBackoffSteps: 8, }, }, - defaults: { - requestsPerMinute: 60, - minTimeBetweenRequests: 350, - concurrentRequests: 6, + providerBreaker: { + oauth: { + failureThreshold: 8, + resetTimeoutMs: 120000, + }, + apikey: { + failureThreshold: 8, + resetTimeoutMs: 60000, + }, }, }, } as const; diff --git a/src/app/(dashboard)/dashboard/health/page.tsx b/src/app/(dashboard)/dashboard/health/page.tsx index 9838f9721d..54efd143e9 100644 --- a/src/app/(dashboard)/dashboard/health/page.tsx +++ b/src/app/(dashboard)/dashboard/health/page.tsx @@ -809,6 +809,9 @@ export default function HealthPage() { {cb.failures === 1 ? t("failures", { count: cb.failures }) : t("failuresPlural", { count: cb.failures })} + {Number(cb.retryAfterMs) > 0 && ( + · retry in {fmtMs(cb.retryAfterMs)} + )} {cb.lastFailure && ( · {t("lastFailure")}:{" "} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 2b9d0abfc4..c0febc8234 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -326,6 +326,7 @@ function anyUpstreamHeadersBadge( interface ModelRowProps { model: { id: string; name?: string; source?: string; isHidden?: boolean }; fullModel: string; + provider: string; copied?: string; onCopy: (text: string, key: string) => void; t: (key: string, values?: Record) => string; @@ -2460,6 +2461,7 @@ export default function ProviderDetailPage() { key={model.id} model={model} fullModel={`${providerDisplayAlias}/${model.id}`} + provider={providerId} copied={copied} onCopy={copy} t={t} @@ -3329,6 +3331,7 @@ export default function ProviderDetailPage() { function ModelRow({ model, fullModel, + provider, copied, onCopy, t, @@ -3407,6 +3410,7 @@ ModelRow.propTypes = { id: PropTypes.string.isRequired, }).isRequired, fullModel: PropTypes.string.isRequired, + provider: PropTypes.string.isRequired, copied: PropTypes.string, onCopy: PropTypes.func.isRequired, t: PropTypes.func, @@ -5402,6 +5406,7 @@ function AddApiKeyModal({ accountId: "", consoleApiKey: "", ccCompatibleContext1m: false, + passthroughModels: false, }); const [validating, setValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); @@ -5495,6 +5500,9 @@ function AddApiKeyModal({ if (formData.excludedModels.trim()) { providerSpecificData.excludedModels = parseExcludedModelsInput(formData.excludedModels); } + if (formData.passthroughModels) { + providerSpecificData.passthroughModels = true; + } if (provider === "bailian-coding-plan" && formData.consoleApiKey.trim()) { providerSpecificData.consoleApiKey = formData.consoleApiKey.trim(); } @@ -5685,6 +5693,13 @@ function AddApiKeyModal({ placeholder="gpt-5*, claude-opus-*, gemini-*-pro*" hint="Comma-separated wildcard patterns. This connection will never serve matching models." /> + setFormData({ ...formData, passthroughModels: checked })} + label={t("perModelQuotaLabel")} + description={t("perModelQuotaDescription")} + /> {provider === "bailian-coding-plan" && ( + setFormData({ ...formData, passthroughModels: checked })} + label={t("perModelQuotaLabel")} + description={t("perModelQuotaDescription")} + /> {connection.provider === "bailian-coding-plan" && ( (null); - const [loading, setLoading] = useState(true); - const [expanded, setExpanded] = useState(false); - const [clearing, setClearing] = useState(null); - const ref = useRef(null); - const notify = useNotificationStore(); - - const fetchStatus = useCallback(async () => { - try { - const res = await fetch("/api/models/availability"); - if (res.ok) { - const json = await res.json(); - setData(json); - } - } catch { - // silent fail — will retry - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchStatus(); - const interval = setInterval(fetchStatus, 30000); - return () => clearInterval(interval); - }, [fetchStatus]); - - // Close popover on outside click - useEffect(() => { - const handleClick = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) { - setExpanded(false); - } - }; - if (expanded) document.addEventListener("mousedown", handleClick); - return () => document.removeEventListener("mousedown", handleClick); - }, [expanded]); - - const handleClearCooldown = async (provider: string, model: string) => { - setClearing(`${provider}:${model}`); - try { - const res = await fetch("/api/models/availability", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "clearCooldown", provider, model }), - }); - if (res.ok) { - notify.success(t("cooldownCleared", { model })); - await fetchStatus(); - } else { - notify.error(t("failedClearCooldown")); - } - } catch { - notify.error(t("failedClearCooldown")); - } finally { - setClearing(null); - } - }; - - if (loading) return null; - - const models = data?.models || []; - const unavailableCount = - data?.unavailableCount || models.filter((m: any) => m.status !== "available").length; - const isHealthy = unavailableCount === 0; - - // Group unhealthy models by provider - const byProvider: Record = {}; - models.forEach((m: any) => { - if (m.status === "available") return; - const key = m.provider || "unknown"; - if (!byProvider[key]) byProvider[key] = []; - byProvider[key].push(m); - }); - - return ( -
- - - {/* Expanded popover */} - {expanded && ( -
-
-
- - {t("modelStatus")} -
- -
- -
- {isHealthy ? ( -

{t("allModelsNormal")}

- ) : ( -
- {Object.entries(byProvider).map(([provider, provModels]) => ( -
-

- {provider} -

-
- {provModels.map((m) => { - const status = - STATUS_CONFIG[m.status as keyof typeof STATUS_CONFIG] || - STATUS_CONFIG.unknown; - const isClearing = clearing === `${m.provider}:${m.model}`; - return ( -
-
- - - {m.model} - -
- {m.status === "cooldown" && ( - - )} -
- ); - })} -
-
- ))} -
- )} -
-
- )} -
- ); -} diff --git a/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityPanel.tsx b/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityPanel.tsx deleted file mode 100644 index 2ede152ba5..0000000000 --- a/src/app/(dashboard)/dashboard/providers/components/ModelAvailabilityPanel.tsx +++ /dev/null @@ -1,203 +0,0 @@ -"use client"; - -/** - * ModelAvailabilityPanel — Batch B - * - * Shows real-time model availability and cooldown status. - * Fetched from /api/models/availability. - */ - -import { useState, useEffect, useCallback } from "react"; -import { useTranslations } from "next-intl"; -import { Card, Button } from "@/shared/components"; -import { useNotificationStore } from "@/store/notificationStore"; - -export default function ModelAvailabilityPanel() { - const t = useTranslations("providers"); - const tc = useTranslations("common"); - - const STATUS_CONFIG = { - available: { icon: "check_circle", color: "#22c55e", label: t("available") }, - cooldown: { icon: "schedule", color: "#f59e0b", label: t("cooldown") }, - unavailable: { icon: "error", color: "#ef4444", label: t("unavailable") }, - unknown: { icon: "help", color: "#6b7280", label: t("unknown") }, - }; - - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [clearing, setClearing] = useState(null); - const notify = useNotificationStore(); - - const fetchStatus = useCallback(async () => { - try { - const res = await fetch("/api/models/availability"); - if (res.ok) { - const json = await res.json(); - setData(json); - } - } catch { - // silent fail — will retry - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchStatus(); - const interval = setInterval(fetchStatus, 30000); - return () => clearInterval(interval); - }, [fetchStatus]); - - const handleClearCooldown = async (provider: string, model: string) => { - setClearing(`${provider}:${model}`); - try { - const res = await fetch("/api/models/availability", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "clearCooldown", provider, model }), - }); - if (res.ok) { - notify.success(t("cooldownCleared", { model })); - await fetchStatus(); - } else { - notify.error(t("failedClearCooldown")); - } - } catch { - notify.error(t("failedClearCooldown")); - } finally { - setClearing(null); - } - }; - - if (loading) { - return ( - -
- - {t("loadingAvailability")} -
-
- ); - } - - const models = data?.models || []; - const unavailableCount = - data?.unavailableCount || models.filter((m: any) => m.status !== "available").length; - - if (models.length === 0 || unavailableCount === 0) { - return ( - -
-
- -
-
-

{t("modelAvailability")}

-

{t("allModelsOperational")}

-
-
-
- ); - } - - // Group by provider - const byProvider: Record = {}; - models.forEach((m: any) => { - if (m.status === "available") return; - const key = m.provider || "unknown"; - if (!byProvider[key]) byProvider[key] = []; - byProvider[key].push(m); - }); - - return ( - -
-
-
- -
-
-

{t("modelAvailability")}

-

- {t("modelsWithIssues", { count: unavailableCount })} -

-
-
- -
- -
- {Object.entries(byProvider).map(([provider, provModels]) => ( -
-

{provider}

-
- {provModels.map((m) => { - const status = - STATUS_CONFIG[m.status as keyof typeof STATUS_CONFIG] || STATUS_CONFIG.unknown; - const isClearing = clearing === `${m.provider}:${m.model}`; - return ( -
-
- - {m.model} - - {status.label} - - {m.cooldownUntil && ( - - {t("until", { time: new Date(m.cooldownUntil).toLocaleTimeString() })} - - )} -
- {m.status === "cooldown" && ( - - )} -
- ); - })} -
-
- ))} -
-
- ); -} diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx index de86e770cb..e509322fa6 100644 --- a/src/app/(dashboard)/dashboard/providers/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/page.tsx @@ -26,7 +26,6 @@ import { getErrorCode, getRelativeTime } from "@/shared/utils"; import { pickDisplayValue } from "@/shared/utils/maskEmail"; import useEmailPrivacyStore from "@/store/emailPrivacyStore"; import { useNotificationStore } from "@/store/notificationStore"; -import ModelAvailabilityBadge from "./components/ModelAvailabilityBadge"; import { useTranslations } from "next-intl"; import { buildMergedOAuthProviderEntries, @@ -496,7 +495,6 @@ export default function ProvidersPage() {
- = { "context-relay": "Context Relay", }; +const LEGACY_COMBO_RESILIENCE_KEYS = new Set([ + "timeoutMs", + "healthCheckEnabled", + "healthCheckTimeoutMs", +]); + function translateOrFallback( t: ReturnType, key: string, @@ -18,14 +24,31 @@ function translateOrFallback( return typeof t.has === "function" && t.has(key) ? t(key) : fallback; } +function sanitizeComboRuntimeConfig(config?: Record | null) { + if (!config || typeof config !== "object") return {}; + return Object.fromEntries( + Object.entries(config).filter( + ([key, value]) => + value !== undefined && value !== null && !LEGACY_COMBO_RESILIENCE_KEYS.has(key) + ) + ); +} + +function sanitizeProviderOverrides(overrides?: Record | null) { + if (!overrides || typeof overrides !== "object") return {}; + return Object.fromEntries( + Object.entries(overrides).map(([providerId, config]) => [ + providerId, + sanitizeComboRuntimeConfig(config), + ]) + ); +} + export default function ComboDefaultsTab() { const [comboDefaults, setComboDefaults] = useState({ strategy: "priority", maxRetries: 1, retryDelayMs: 2000, - timeoutMs: 120000, - healthCheckEnabled: true, - healthCheckTimeoutMs: 3000, maxComboDepth: 3, trackMetrics: true, handoffThreshold: 0.85, @@ -54,7 +77,6 @@ export default function ComboDefaultsTab() { const numericSettings = [ { key: "maxRetries", label: t("maxRetriesLabel"), min: 0, max: 5 }, { key: "retryDelayMs", label: t("retryDelayLabel"), min: 500, max: 10000, step: 500 }, - { key: "timeoutMs", label: t("timeoutLabel"), min: 5000, step: 5000 }, { key: "maxComboDepth", label: t("maxNestingDepth"), min: 1, max: 10 }, ]; @@ -66,7 +88,7 @@ export default function ComboDefaultsTab() { .then(([comboData, settingsData]) => { setComboDefaults((prev) => ({ ...prev, - ...(comboData.comboDefaults || {}), + ...sanitizeComboRuntimeConfig(comboData.comboDefaults), strategy: settingsData.fallbackStrategy ?? comboData.comboDefaults?.strategy ?? prev.strategy, stickyRoundRobinLimit: @@ -74,7 +96,9 @@ export default function ComboDefaultsTab() { comboData.comboDefaults?.stickyRoundRobinLimit ?? prev.stickyRoundRobinLimit, })); - if (comboData.providerOverrides) setProviderOverrides(comboData.providerOverrides); + if (comboData.providerOverrides) { + setProviderOverrides(sanitizeProviderOverrides(comboData.providerOverrides)); + } }) .catch((err) => console.error("Failed to fetch combo defaults:", err)); }, []); @@ -116,7 +140,10 @@ export default function ComboDefaultsTab() { const comboDefaultsRes = await fetch("/api/settings/combo-defaults", { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ comboDefaults: comboDefaultsPayload, providerOverrides }), + body: JSON.stringify({ + comboDefaults: sanitizeComboRuntimeConfig(comboDefaultsPayload), + providerOverrides: sanitizeProviderOverrides(providerOverrides), + }), }); if (!comboDefaultsRes.ok) { @@ -136,7 +163,7 @@ export default function ComboDefaultsTab() { const addProviderOverride = () => { const name = newOverrideProvider.trim().toLowerCase(); if (!name || providerOverrides[name]) return; - setProviderOverrides((prev) => ({ ...prev, [name]: { maxRetries: 1, timeoutMs: 120000 } })); + setProviderOverrides((prev) => ({ ...prev, [name]: { maxRetries: 1 } })); setNewOverrideProvider(""); }; @@ -381,21 +408,6 @@ export default function ComboDefaultsTab() { {/* Toggles */}
-
-
-

{t("healthCheck")}

-

{t("healthCheckDesc")}

-
- - setComboDefaults((prev) => ({ - ...prev, - healthCheckEnabled: !prev.healthCheckEnabled, - })) - } - /> -

{t("trackMetrics")}

@@ -436,24 +448,6 @@ export default function ComboDefaultsTab() { aria-label={t("providerMaxRetriesAria", { provider })} /> {t("retries")} - - setProviderOverrides((prev) => ({ - ...prev, - [provider]: { - ...prev[provider], - timeoutMs: parseInt(e.target.value) || 120000, - }, - })) - } - className="text-xs w-24" - aria-label={t("providerTimeoutAria", { provider })} - /> - {t("ms")}
@@ -106,7 +98,7 @@ export default function PoliciesPanel() { gpp_maybe
-

{t("policiesCircuitBreakers")}

+

{t("policiesLocked")}

{t("activeIssuesDetected")}

@@ -115,50 +107,6 @@ export default function PoliciesPanel() {
- {/* Circuit Breakers */} - {circuitBreakers.filter((cb) => cb.state !== "closed").length > 0 && ( -
-

{t("circuitBreakers")}

-
- {circuitBreakers - .filter((cb) => cb.state !== "closed") - .map((cb, i) => { - const status = CB_STATUS[cb.state] || CB_STATUS.open; - return ( -
-
- - {status.icon} - - - {cb.name || cb.provider || "Unknown"} - - - {status.label} - - {cb.failures > 0 && ( - {cb.failures} failures - )} -
-
- ); - })} -
-
- )} - {/* Locked Identifiers */} {lockedIds.length > 0 && (
diff --git a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx index e286af443b..f3ca6f7105 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx @@ -1,549 +1,316 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; -import { Card, Button } from "@/shared/components"; +import { type ReactNode, useEffect, useState } from "react"; +import { Button, Card } from "@/shared/components"; import { useNotificationStore } from "@/store/notificationStore"; -import { useLocale, useTranslations } from "next-intl"; -import AutoDisableCard from "./AutoDisableCard"; +import { useTranslations } from "next-intl"; -// ─── State colors and labels ────────────────────────────────────────────── -const STATE_STYLES = { - CLOSED: { - bg: "bg-emerald-500/15", - text: "text-emerald-400", - border: "border-emerald-500/30", - icon: "check_circle", - }, - OPEN: { - bg: "bg-red-500/15", - text: "text-red-400", - border: "border-red-500/30", - icon: "error", - }, - HALF_OPEN: { - bg: "bg-amber-500/15", - text: "text-amber-400", - border: "border-amber-500/30", - icon: "warning", - }, +type RequestQueueSettings = { + autoEnableApiKeyProviders: boolean; + requestsPerMinute: number; + minTimeBetweenRequestsMs: number; + concurrentRequests: number; + maxWaitMs: number; }; -const CB_STATUS = { - closed: { icon: "check_circle", color: "#22c55e" }, - "half-open": { icon: "pending", color: "#f59e0b" }, - open: { icon: "error", color: "#ef4444" }, +type ConnectionCooldownProfileSettings = { + baseCooldownMs: number; + useUpstreamRetryHints: boolean; + maxBackoffSteps: number; }; -function getBreakerStateLabel(state, t) { - const normalized = String(state || "closed") - .toLowerCase() - .replaceAll("_", "-"); - if (normalized === "open") return t("breakerStateOpen"); - if (normalized === "half-open") return t("breakerStateHalfOpen"); - return t("breakerStateClosed"); +type ProviderBreakerProfileSettings = { + failureThreshold: number; + resetTimeoutMs: number; +}; + +type WaitForCooldownSettings = { + enabled: boolean; + maxRetries: number; + maxRetryWaitSec: number; +}; + +type ResilienceResponse = { + requestQueue: RequestQueueSettings; + connectionCooldown: { + oauth: ConnectionCooldownProfileSettings; + apikey: ConnectionCooldownProfileSettings; + }; + providerBreaker: { + oauth: ProviderBreakerProfileSettings; + apikey: ProviderBreakerProfileSettings; + }; + waitForCooldown: WaitForCooldownSettings; +}; + +function formatMs(value: number | null | undefined) { + if (typeof value !== "number") return "—"; + return `${value}ms`; } -function formatMs(ms) { - if (!ms || ms <= 0) return "—"; - if (ms < 1000) return `${ms}ms`; - if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; - return `${(ms / 60000).toFixed(1)}m`; +function SectionDescription({ + scope, + trigger, + effect, +}: { + scope: string; + trigger: string; + effect: string; +}) { + return ( +
+
+ Scope: {scope} +
+
+ Trigger: {trigger} +
+
+ Effect: {effect} +
+
+ ); } -function getErrorMessage(err, fallback) { - return err instanceof Error && err.message ? err.message : fallback; +function NumberField({ + label, + value, + suffix, + min = 0, + onChange, +}: { + label: string; + value: number; + suffix?: string; + min?: number; + onChange: (value: number) => void; +}) { + return ( + + ); } -// ─── Provider Profiles Card ────────────────────────────────────────────── -function ProviderProfilesCard({ profiles, onSave, saving }) { - const [editMode, setEditMode] = useState(false); - const [draft, setDraft] = useState(profiles); - const t = useTranslations("settings"); +function BooleanField({ + label, + description, + checked, + onChange, +}: { + label: string; + description: string; + checked: boolean; + onChange: (value: boolean) => void; +}) { + return ( + + ); +} + +function ProfileColumn({ + title, + icon, + children, +}: { + title: string; + icon: string; + children: ReactNode; +}) { + return ( +
+
+ {icon} +

{title}

+
+
{children}
+
+ ); +} + +function ActionRow({ + editing, + saving, + onEdit, + onCancel, + onSave, +}: { + editing: boolean; + saving: boolean; + onEdit: () => void; + onCancel: () => void; + onSave: () => void; +}) { const tc = useTranslations("common"); - - useEffect(() => { - setDraft(profiles); - }, [profiles]); - - const formatMsRaw = (value) => (value == null ? "—" : `${value}${t("ms")}`); - const fields = [ - { key: "transientCooldown", label: t("transientCooldown"), format: formatMsRaw }, - { key: "rateLimitCooldown", label: t("rateLimitCooldown"), format: formatMsRaw }, - { key: "maxBackoffLevel", label: t("maxBackoffLevel") }, - { - key: "circuitBreakerThreshold", - label: t("cbThreshold"), - format: (value) => (value == null ? "—" : t("failures", { count: value })), - }, - { key: "circuitBreakerReset", label: t("cbResetTime"), format: formatMsRaw }, - ]; - - const handleSave = () => { - // Only send 'oauth' and 'apikey' — the API schema rejects any other keys (e.g. 'local') - const { oauth, apikey } = draft ?? {}; - onSave({ ...(oauth ? { oauth } : {}), ...(apikey ? { apikey } : {}) }); - setEditMode(false); - }; - - return ( - -
-
-
- -

{t("providerProfiles")}

-
- {editMode ? ( -
- - -
- ) : ( - - )} -
- -

{t("providerProfilesDesc")}

- -
- {["oauth", "apikey"].map((type) => ( -
-

- - {type === "oauth" ? t("oauthProviders") : t("apiKeyProviders")} -

-
- {fields.map(({ key, label, format }) => ( -
- {label} - {editMode ? ( - - setDraft({ - ...draft, - [type]: { ...draft[type], [key]: Number(e.target.value) }, - }) - } - className="w-24 px-2 py-1 text-xs rounded bg-white/10 border border-white/20 text-right" - /> - ) : ( - - {format - ? format(profiles?.[type]?.[key]) - : (profiles?.[type]?.[key] ?? "—")} - - )} -
- ))} -
-
- ))} -
-
-
- ); -} - -// ─── Editable Rate Limit Card ───────────────────────────────────────────── -function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) { - const [editMode, setEditMode] = useState(false); - const [draft, setDraft] = useState(defaults || {}); - const t = useTranslations("settings"); - const tc = useTranslations("common"); - - // Sync draft when defaults change from parent (standard prop-to-state sync) - /* eslint-disable react-hooks/set-state-in-effect */ - useEffect(() => { - if (defaults) setDraft(defaults); - }, [defaults]); - /* eslint-enable react-hooks/set-state-in-effect */ - - const handleSave = () => { - onSaveDefaults(draft); - setEditMode(false); - }; - - return ( - -
-
-
- -

{t("rateLimiting")}

-
- {editMode ? ( -
- - -
- ) : ( - - )} -
- -

{t("rateLimitingDesc")}

- -
-

- {t("defaultSafetyNet")} -

-
- {[ - { key: "requestsPerMinute", label: t("rpm") }, - { key: "minTimeBetweenRequests", label: t("minGap"), format: formatMs }, - { key: "concurrentRequests", label: t("maxConcurrent") }, - ].map(({ key, label, format }) => ( -
- {editMode ? ( - - setDraft((prev) => ({ ...prev, [key]: parseInt(e.target.value) || 0 })) - } - className="w-full px-2 py-1 text-lg font-bold rounded bg-white/10 border border-white/20" - /> - ) : ( -
- {format ? format(defaults?.[key]) : (defaults?.[key] ?? "—")} -
- )} -
{label}
-
- ))} -
-
- - {rateLimitStatus && rateLimitStatus.length > 0 ? ( -
-

- {t("activeLimiters")} -

- {rateLimitStatus.map((rl, i) => ( -
- {rl.provider || rl.key} -
- {rl.reservoir != null && ( - - {t("reservoir")}: {rl.reservoir} - - )} - {rl.running != null && ( - - {t("running")}: {rl.running} - - )} - {rl.queued != null && ( - - {t("queued")}: {rl.queued} - - )} -
-
- ))} -
- ) : ( -

{t("noActiveLimiters")}

- )} -
-
- ); -} - -// ─── Circuit Breaker Card ──────────────────────────────────────────────── -function CircuitBreakerCard({ breakers, onReset, loading }) { - const activeBreakers = breakers.filter((b) => b.state !== "CLOSED"); - const totalBreakers = breakers.length; - const t = useTranslations("settings"); - - return ( - -
-
-
- -

{t("circuitBreakers")}

-
-
- - {activeBreakers.length > 0 - ? t("tripped", { count: activeBreakers.length }) - : t("healthy", { count: totalBreakers })} - - {activeBreakers.length > 0 && ( - - )} -
-
- - {breakers.length === 0 ? ( -

{t("noCircuitBreakers")}

- ) : ( -
- {breakers.map((b) => { - const style = STATE_STYLES[b.state] || STATE_STYLES.CLOSED; - return ( -
-
- - {b.name.replace("combo:", "")} -
-
- {b.failureCount > 0 && ( - - {t("failures", { count: b.failureCount })} - - )} - - {getBreakerStateLabel(b.state, t)} - -
-
- ); - })} -
- )} -
-
- ); -} - -// ─── Policies Panel (from Security tab) ────────────────────────────────── -function PoliciesCard() { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [unlocking, setUnlocking] = useState(null); - const notify = useNotificationStore(); - const locale = useLocale(); - const t = useTranslations("settings"); - - const fetchPolicies = useCallback(async () => { - try { - const res = await fetch("/api/policies"); - if (res.ok) { - const json = await res.json(); - setData(json); - } - } catch { - // silent - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchPolicies(); - const interval = setInterval(fetchPolicies, 15000); - return () => clearInterval(interval); - }, [fetchPolicies]); - - const handleUnlock = async (identifier) => { - setUnlocking(identifier); - try { - const res = await fetch("/api/policies", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "unlock", identifier }), - }); - if (res.ok) { - notify.success(t("unlockedIdentifier", { identifier })); - await fetchPolicies(); - } else { - notify.error(t("failedUnlock")); - } - } catch { - notify.error(t("failedUnlock")); - } finally { - setUnlocking(null); - } - }; - - const circuitBreakers = data?.circuitBreakers || []; - const lockedIds = data?.lockedIdentifiers || []; - const hasIssues = circuitBreakers.some((cb) => cb.state !== "closed") || lockedIds.length > 0; - - if (loading) { + if (editing) { return ( - -
- policy - {t("loadingPolicies")} -
-
+
+ + +
); } return ( - -
-
-
- -

{t("policiesLocked")}

-
- {hasIssues && ( - - )} -
+ + ); +} - {!hasIssues ? ( -
-
- verified_user -
-
-

{t("allOperational")}

-
+function RequestQueueCard({ + value, + onSave, + saving, +}: { + value: RequestQueueSettings; + onSave: (next: RequestQueueSettings) => Promise; + saving: boolean; +}) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(value); + + useEffect(() => { + setDraft(value); + }, [value]); + + return ( + +
+
+
+ speed +

Request Queue & Pacing

+ +
+ setEditing(true)} + onCancel={() => { + setDraft(value); + setEditing(false); + }} + onSave={async () => { + await onSave(draft); + setEditing(false); + }} + /> +
+ +

+ This layer only controls queueing and pacing. It does not write cooldowns or open breakers. +

+ +
+ {editing ? ( + <> + + setDraft((prev) => ({ ...prev, autoEnableApiKeyProviders })) + } + /> + setDraft((prev) => ({ ...prev, requestsPerMinute }))} + /> + + setDraft((prev) => ({ ...prev, minTimeBetweenRequestsMs })) + } + /> + + setDraft((prev) => ({ ...prev, concurrentRequests })) + } + /> + setDraft((prev) => ({ ...prev, maxWaitMs }))} + /> + ) : ( <> - {/* Circuit Breakers */} - {circuitBreakers.filter((cb) => cb.state !== "closed").length > 0 && ( -
-

{t("circuitBreakers")}

-
- {circuitBreakers - .filter((cb) => cb.state !== "closed") - .map((cb, i) => { - const status = CB_STATUS[cb.state] || CB_STATUS.open; - return ( -
-
- - {status.icon} - - - {cb.name || cb.provider || t("unknown")} - - - {getBreakerStateLabel(cb.state, t)} - - {cb.failures > 0 && ( - - {t("failures", { count: cb.failures })} - - )} -
-
- ); - })} -
+
+
Auto-enable for API key providers
+
+ {value.autoEnableApiKeyProviders ? "Enabled" : "Disabled"}
- )} - - {/* Locked Identifiers */} - {lockedIds.length > 0 && ( -
-

{t("lockedIdentifiers")}

-
- {lockedIds.map((id, i) => { - const identifier = typeof id === "string" ? id : id.identifier || id.id; - return ( -
-
- - lock - - {identifier} - {typeof id === "object" && id.lockedAt && ( - - {t("sinceDate", { - date: new Date(id.lockedAt).toLocaleString(locale), - })} - - )} -
- -
- ); - })} -
+
+
+
Requests per minute
+
+ {value.requestsPerMinute}
- )} +
+
+
Min time between requests
+
+ {formatMs(value.minTimeBetweenRequestsMs)} +
+
+
+
Concurrent requests
+
+ {value.concurrentRequests} +
+
+
+
Max queue wait
+
+ {formatMs(value.maxWaitMs)} +
+
)}
@@ -551,131 +318,435 @@ function PoliciesCard() { ); } -// ─── Main Resilience Tab ───────────────────────────────────────────────── -export default function ResilienceTab() { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); - const t = useTranslations("settings"); - - const loadData = useCallback(async () => { - try { - setLoading(true); - const res = await fetch("/api/resilience"); - if (!res.ok) throw new Error(t("failedLoadWithStatus", { status: res.status })); - const json = await res.json(); - setData(json); - setError(null); - } catch (err) { - setError(getErrorMessage(err, t("failedLoadResilience"))); - } finally { - setLoading(false); - } - }, [t]); +function ConnectionCooldownCard({ + value, + onSave, + saving, +}: { + value: ResilienceResponse["connectionCooldown"]; + onSave: (next: ResilienceResponse["connectionCooldown"]) => Promise; + saving: boolean; +}) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(value); useEffect(() => { - loadData(); - // Auto-refresh every 10s - const interval = setInterval(loadData, 10000); - return () => clearInterval(interval); - }, [loadData]); + setDraft(value); + }, [value]); - const handleResetBreakers = async () => { - try { - setLoading(true); - const res = await fetch("/api/resilience/reset", { method: "POST" }); - if (!res.ok) throw new Error(t("resetFailed")); - await loadData(); - } catch (err) { - setError(getErrorMessage(err, t("resetFailed"))); - } finally { - setLoading(false); - } + const renderProfile = (key: "oauth" | "apikey", title: string, icon: string) => { + const current = editing ? draft[key] : value[key]; + return ( + + {editing ? ( + <> + + setDraft((prev) => ({ ...prev, [key]: { ...prev[key], baseCooldownMs } })) + } + /> + + setDraft((prev) => ({ + ...prev, + [key]: { ...prev[key], useUpstreamRetryHints }, + })) + } + /> + + setDraft((prev) => ({ ...prev, [key]: { ...prev[key], maxBackoffSteps } })) + } + /> + + ) : ( + <> +
+ Base cooldown + {formatMs(current.baseCooldownMs)} +
+
+ Use upstream retry hints + + {current.useUpstreamRetryHints ? "Yes" : "No"} + +
+
+ Max backoff steps + {current.maxBackoffSteps} +
+ + )} +
+ ); }; - const handleSaveProfiles = async (profiles) => { - try { - setSaving(true); - const res = await fetch("/api/resilience", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ profiles }), - }); - if (!res.ok) throw new Error(t("saveFailed")); - await loadData(); - } catch (err) { - setError(getErrorMessage(err, t("saveFailed"))); - } finally { - setSaving(false); - } + return ( + +
+
+
+ timer_off +

Connection Cooldown

+
+ +
+ setEditing(true)} + onCancel={() => { + setDraft(value); + setEditing(false); + }} + onSave={async () => { + await onSave(draft); + setEditing(false); + }} + /> +
+ +

+ Base cooldown covers retryable connection failures. When upstream retry hints are enabled, + explicit provider wait windows override the local base cooldown. +

+ +
+ {renderProfile("oauth", "OAuth Providers", "lock")} + {renderProfile("apikey", "API Key Providers", "key")} +
+
+ ); +} + +function ProviderBreakerCard({ + value, + onSave, + saving, +}: { + value: ResilienceResponse["providerBreaker"]; + onSave: (next: ResilienceResponse["providerBreaker"]) => Promise; + saving: boolean; +}) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(value); + + useEffect(() => { + setDraft(value); + }, [value]); + + const renderProfile = (key: "oauth" | "apikey", title: string, icon: string) => { + const current = editing ? draft[key] : value[key]; + return ( + + {editing ? ( + <> + + setDraft((prev) => ({ ...prev, [key]: { ...prev[key], failureThreshold } })) + } + /> + + setDraft((prev) => ({ ...prev, [key]: { ...prev[key], resetTimeoutMs } })) + } + /> + + ) : ( + <> +
+ Failure threshold + {current.failureThreshold} +
+
+ Reset timeout + {formatMs(current.resetTimeoutMs)} +
+ + )} +
+ ); }; - const handleSaveDefaults = async (defaults) => { + return ( + +
+
+
+ + electrical_services + +

Provider Circuit Breaker

+
+ +
+ setEditing(true)} + onCancel={() => { + setDraft(value); + setEditing(false); + }} + onSave={async () => { + await onSave(draft); + setEditing(false); + }} + /> +
+ +

+ Breaker runtime state is shown only on the Health page. Connection-scoped 429 rate limits + stay in Connection Cooldown and do not trip the provider breaker. +

+ +
+ {renderProfile("oauth", "OAuth Providers", "lock")} + {renderProfile("apikey", "API Key Providers", "key")} +
+
+ ); +} + +function WaitForCooldownCard({ + value, + onSave, + saving, +}: { + value: WaitForCooldownSettings; + onSave: (next: WaitForCooldownSettings) => Promise; + saving: boolean; +}) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(value); + + useEffect(() => { + setDraft(value); + }, [value]); + + return ( + +
+
+
+ hourglass_top +

Wait For Cooldown

+
+ +
+ setEditing(true)} + onCancel={() => { + setDraft(value); + setEditing(false); + }} + onSave={async () => { + await onSave(draft); + setEditing(false); + }} + /> +
+ +

+ This only affects the current request. It does not write connection or provider state. +

+ +
+ {editing ? ( + <> + setDraft((prev) => ({ ...prev, enabled }))} + /> + setDraft((prev) => ({ ...prev, maxRetries }))} + /> + setDraft((prev) => ({ ...prev, maxRetryWaitSec }))} + /> + + ) : ( + <> +
+
Enable server-side waiting
+
+ {value.enabled ? "Enabled" : "Disabled"} +
+
+
+
Max retries
+
{value.maxRetries}
+
+
+
Max retry wait
+
+ {value.maxRetryWaitSec}s +
+
+ + )} +
+
+ ); +} + +export default function ResilienceTab() { + const notify = useNotificationStore(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [savingSection, setSavingSection] = useState(null); + + useEffect(() => { + let mounted = true; + + const load = async () => { + try { + const response = await fetch("/api/resilience"); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const json = await response.json(); + if (!mounted) return; + setData({ + requestQueue: json.requestQueue, + connectionCooldown: json.connectionCooldown, + providerBreaker: json.providerBreaker, + waitForCooldown: json.waitForCooldown, + }); + } catch (error) { + notify.error(error instanceof Error ? error.message : "Failed to load resilience settings"); + } finally { + if (mounted) setLoading(false); + } + }; + + void load(); + return () => { + mounted = false; + }; + }, [notify]); + + const savePatch = async (section: string, payload: Record) => { + setSavingSection(section); try { - setSaving(true); - const res = await fetch("/api/resilience", { + const response = await fetch("/api/resilience", { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ defaults }), + body: JSON.stringify(payload), }); - if (!res.ok) throw new Error(t("saveFailed")); - await loadData(); - } catch (err) { - setError(getErrorMessage(err, t("saveFailed"))); + const json = await response.json(); + if (!response.ok) { + throw new Error(json?.error?.message || json?.error || `HTTP ${response.status}`); + } + setData({ + requestQueue: json.requestQueue, + connectionCooldown: json.connectionCooldown, + providerBreaker: json.providerBreaker, + waitForCooldown: json.waitForCooldown, + }); + notify.success("Resilience settings updated."); + } catch (error) { + notify.error(error instanceof Error ? error.message : "Failed to save resilience settings"); + throw error; } finally { - setSaving(false); + setSavingSection(null); } }; if (loading && !data) { return ( -
- hourglass_empty - {t("loadingResilience")} -
+ +
+ progress_activity + Loading resilience settings... +
+
); } - if (error && !data) { + if (!data) { return ( -
- error - {error} -
- +

Unable to load resilience settings.

); } return ( -
- {/* 1. Provider Profiles (resilience settings by auth type) */} - + +
+ info +
+

Resilience Structure

+

+ This page only configures behavior. Live breaker state is shown on the Health page. + Combo-specific retry and round-robin slot control remain on combo settings. +

+
+
+
+ + savePatch("requestQueue", { requestQueue })} /> - {/* 1.5 Auto Disable Banned Accounts */} - - {/* 2. Rate Limiting (editable defaults + active limiters) */} - savePatch("connectionCooldown", { connectionCooldown })} /> - {/* 3. Circuit Breakers (combo pipeline) */} - savePatch("providerBreaker", { providerBreaker })} + /> + savePatch("waitForCooldown", { waitForCooldown })} /> - {/* 4. Policies & Locked Identifiers (from previous Security tab) */} -
); } diff --git a/src/app/api/cli-tools/codex-settings/route.ts b/src/app/api/cli-tools/codex-settings/route.ts index deac929dae..5cf1151a1d 100644 --- a/src/app/api/cli-tools/codex-settings/route.ts +++ b/src/app/api/cli-tools/codex-settings/route.ts @@ -38,9 +38,16 @@ const parseToml = (content: string) => { // Key = value const kvMatch = trimmed.match(/^([^=]+)\s*=\s*(.+)$/); if (kvMatch) { - const key = kvMatch[1].trim(); + let key = kvMatch[1].trim(); let value = kvMatch[2].trim(); - // Remove quotes + // Strip quotes from key (TOML quoted keys like "gpt-5.3-codex") + if ( + (key.startsWith('"') && key.endsWith('"')) || + (key.startsWith("'") && key.endsWith("'")) + ) { + key = key.slice(1, -1); + } + // Remove quotes from string values only (not arrays, booleans, numbers) if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) @@ -58,13 +65,23 @@ const parseToml = (content: string) => { return result; }; +// Format a TOML value: arrays and booleans stay unquoted, strings get quoted +const formatTomlValue = (value: unknown): string => { + if (typeof value === "boolean") return value ? "true" : "false"; + if (typeof value === "number") return String(value); + // Preserve pre-formatted TOML arrays (e.g. ["a", "b"]) + if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) return value; + if (typeof value === "string") return `"${value}"`; + return `"${value}"`; +}; + // Convert parsed object back to TOML string const toToml = (parsed: Record) => { let lines: string[] = []; // Root level keys Object.entries(parsed._root).forEach(([key, value]) => { - lines.push(`${key} = "${value}"`); + lines.push(`${key} = ${formatTomlValue(value)}`); }); // Sections @@ -73,7 +90,7 @@ const toToml = (parsed: Record) => { lines.push(`[${section}]`); Object.entries(values).forEach(([key, value]) => { const formattedKey = key.includes(".") ? `"${key}"` : key; - lines.push(`${formattedKey} = "${value}"`); + lines.push(`${formattedKey} = ${formatTomlValue(value)}`); }); }); diff --git a/src/app/api/models/availability/route.ts b/src/app/api/models/availability/route.ts deleted file mode 100644 index 9ef7418c54..0000000000 --- a/src/app/api/models/availability/route.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NextResponse } from "next/server"; -import { - getAvailabilityReport, - clearModelUnavailability, - getUnavailableCount, -} from "@/domain/modelAvailability"; -import { clearModelAvailabilitySchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; - -export async function GET() { - try { - const report = getAvailabilityReport(); - const count = getUnavailableCount(); - return NextResponse.json({ unavailableCount: count, models: report }); - } catch (error) { - console.error("Error getting model availability:", error); - return NextResponse.json({ error: "Failed to get model availability" }, { status: 500 }); - } -} - -export async function POST(request) { - let rawBody; - try { - rawBody = await request.json(); - } catch { - return NextResponse.json( - { - error: { - message: "Invalid request", - details: [{ field: "body", message: "Invalid JSON body" }], - }, - }, - { status: 400 } - ); - } - - try { - const validation = validateBody(clearModelAvailabilitySchema, rawBody); - if (isValidationFailure(validation)) { - return NextResponse.json({ error: validation.error }, { status: 400 }); - } - const { provider, model } = validation.data; - - const removed = clearModelUnavailability(provider, model); - return NextResponse.json({ success: true, removed }); - } catch (error) { - console.error("Error clearing model availability:", error); - return NextResponse.json({ error: "Failed to clear model availability" }, { status: 500 }); - } -} diff --git a/src/app/api/policies/route.ts b/src/app/api/policies/route.ts index dc74eb76f6..47f0e3dfa5 100644 --- a/src/app/api/policies/route.ts +++ b/src/app/api/policies/route.ts @@ -1,14 +1,12 @@ import { NextResponse } from "next/server"; -import { getAllCircuitBreakerStatuses } from "@/shared/utils/circuitBreaker"; import { getLockedIdentifiers, forceUnlock } from "@/domain/lockoutPolicy"; import { policyActionSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; export async function GET() { try { - const circuitBreakers = getAllCircuitBreakerStatuses(); const lockedIdentifiers = getLockedIdentifiers(); - return NextResponse.json({ circuitBreakers, lockedIdentifiers }); + return NextResponse.json({ lockedIdentifiers }); } catch (error) { console.error("Error loading policies:", error); return NextResponse.json({ error: "Failed to load policies" }, { status: 500 }); diff --git a/src/app/api/resilience/reset/route.ts b/src/app/api/resilience/reset/route.ts index 3ee4e94aea..8c21960732 100644 --- a/src/app/api/resilience/reset/route.ts +++ b/src/app/api/resilience/reset/route.ts @@ -1,11 +1,10 @@ import { NextResponse } from "next/server"; /** - * POST /api/resilience/reset — Reset all circuit breakers and clear cooldowns + * POST /api/resilience/reset — Reset all provider circuit breakers */ export async function POST() { try { - // Reset all circuit breakers const { getAllCircuitBreakerStatuses, getCircuitBreaker } = await import("@/shared/utils/circuitBreaker"); diff --git a/src/app/api/resilience/route.ts b/src/app/api/resilience/route.ts index d1bea74e33..9436aae3cf 100644 --- a/src/app/api/resilience/route.ts +++ b/src/app/api/resilience/route.ts @@ -1,5 +1,12 @@ import { NextResponse } from "next/server"; import { getSettings, updateSettings } from "@/lib/localDb"; +import { + buildLegacyResilienceCompat, + mergeResilienceSettings, + resolveResilienceSettings, + type ResilienceSettings, + type ResilienceSettingsPatch, +} from "@/lib/resilience/settings"; import { updateResilienceSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; @@ -13,41 +20,130 @@ function getErrorMessage(error: unknown, fallback: string): string { return error instanceof Error && error.message ? error.message : fallback; } +function normalizeLegacyPatch(body: JsonRecord): ResilienceSettingsPatch { + const profiles = asRecord(body.profiles); + const defaults = asRecord(body.defaults); + const oauth = asRecord(profiles.oauth); + const apikey = asRecord(profiles.apikey); + + const patch: ResilienceSettingsPatch = {}; + + if (Object.keys(defaults).length > 0) { + patch.requestQueue = { + ...(typeof defaults.requestsPerMinute === "number" + ? { requestsPerMinute: defaults.requestsPerMinute } + : {}), + ...(typeof defaults.minTimeBetweenRequests === "number" + ? { minTimeBetweenRequestsMs: defaults.minTimeBetweenRequests } + : {}), + ...(typeof defaults.concurrentRequests === "number" + ? { concurrentRequests: defaults.concurrentRequests } + : {}), + }; + } + + if (Object.keys(oauth).length > 0 || Object.keys(apikey).length > 0) { + const buildLegacyCooldownPatch = (profile: JsonRecord) => { + const cooldownCandidates = [ + typeof profile.transientCooldown === "number" ? profile.transientCooldown : null, + typeof profile.rateLimitCooldown === "number" && profile.rateLimitCooldown > 0 + ? profile.rateLimitCooldown + : null, + ].filter((value): value is number => typeof value === "number"); + + return { + ...(cooldownCandidates.length > 0 + ? { baseCooldownMs: Math.max(...cooldownCandidates) } + : {}), + ...(typeof profile.rateLimitCooldown === "number" + ? { useUpstreamRetryHints: profile.rateLimitCooldown === 0 } + : {}), + ...(typeof profile.maxBackoffLevel === "number" + ? { maxBackoffSteps: profile.maxBackoffLevel } + : {}), + }; + }; + + patch.connectionCooldown = { + ...(Object.keys(oauth).length > 0 + ? { + oauth: buildLegacyCooldownPatch(oauth), + } + : {}), + ...(Object.keys(apikey).length > 0 + ? { + apikey: buildLegacyCooldownPatch(apikey), + } + : {}), + }; + + patch.providerBreaker = { + ...(Object.keys(oauth).length > 0 + ? { + oauth: { + ...(typeof oauth.circuitBreakerThreshold === "number" + ? { failureThreshold: oauth.circuitBreakerThreshold } + : {}), + ...(typeof oauth.circuitBreakerReset === "number" + ? { resetTimeoutMs: oauth.circuitBreakerReset } + : {}), + }, + } + : {}), + ...(Object.keys(apikey).length > 0 + ? { + apikey: { + ...(typeof apikey.circuitBreakerThreshold === "number" + ? { failureThreshold: apikey.circuitBreakerThreshold } + : {}), + ...(typeof apikey.circuitBreakerReset === "number" + ? { resetTimeoutMs: apikey.circuitBreakerReset } + : {}), + }, + } + : {}), + }; + } + + return patch; +} + +async function syncRuntimeSettings(resilienceSettings: ResilienceSettings) { + const { applyRequestQueueSettings } = + await import("@omniroute/open-sse/services/rateLimitManager"); + applyRequestQueueSettings(resilienceSettings.requestQueue); +} + /** - * GET /api/resilience — Get current resilience configuration and status + * GET /api/resilience — Get current resilience configuration */ export async function GET() { try { - // Dynamic imports for open-sse modules - const { getAllCircuitBreakerStatuses } = await import("@/shared/utils/circuitBreaker"); - const { getAllRateLimitStatus } = await import("@omniroute/open-sse/services/rateLimitManager"); - const { PROVIDER_PROFILES, DEFAULT_API_LIMITS } = - await import("@omniroute/open-sse/config/constants"); - const settings = await getSettings(); - const circuitBreakers = getAllCircuitBreakerStatuses(); - const rateLimitStatus = getAllRateLimitStatus(); + const resilience = resolveResilienceSettings(settings); return NextResponse.json({ - profiles: settings.providerProfiles || PROVIDER_PROFILES, - defaults: { - ...DEFAULT_API_LIMITS, - ...asRecord(settings.rateLimitDefaults), + requestQueue: resilience.requestQueue, + connectionCooldown: resilience.connectionCooldown, + providerBreaker: resilience.providerBreaker, + waitForCooldown: { + enabled: resilience.waitForCooldown.enabled, + maxRetries: resilience.waitForCooldown.maxRetries, + maxRetryWaitSec: resilience.waitForCooldown.maxRetryWaitSec, }, - circuitBreakers, - rateLimitStatus, + legacy: buildLegacyResilienceCompat(resilience), }); } catch (err: unknown) { console.error("[API] GET /api/resilience error:", err); return NextResponse.json( - { error: getErrorMessage(err, "Failed to load resilience status") }, + { error: getErrorMessage(err, "Failed to load resilience settings") }, { status: 500 } ); } } /** - * PATCH /api/resilience — Update provider resilience profiles and/or rate limit defaults + * PATCH /api/resilience — Update resilience configuration */ export async function PATCH(request) { let rawBody; @@ -70,18 +166,47 @@ export async function PATCH(request) { if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } - const { profiles, defaults } = validation.data; - const updates: Record = {}; - if (profiles) updates.providerProfiles = profiles; - if (defaults) updates.rateLimitDefaults = defaults; + const body = validation.data as JsonRecord; + const currentSettings = await getSettings(); + const currentResilience = resolveResilienceSettings(currentSettings); + const nextResilience = mergeResilienceSettings(currentResilience, { + ...(body.requestQueue + ? { requestQueue: body.requestQueue as ResilienceSettingsPatch["requestQueue"] } + : {}), + ...(body.connectionCooldown + ? { + connectionCooldown: + body.connectionCooldown as ResilienceSettingsPatch["connectionCooldown"], + } + : {}), + ...(body.providerBreaker + ? { providerBreaker: body.providerBreaker as ResilienceSettingsPatch["providerBreaker"] } + : {}), + ...(body.waitForCooldown + ? { waitForCooldown: body.waitForCooldown as ResilienceSettingsPatch["waitForCooldown"] } + : {}), + ...normalizeLegacyPatch(body), + }); - await updateSettings(updates); + await updateSettings({ + resilienceSettings: nextResilience, + requestRetry: nextResilience.waitForCooldown.maxRetries, + maxRetryIntervalSec: nextResilience.waitForCooldown.maxRetryWaitSec, + }); + await syncRuntimeSettings(nextResilience); return NextResponse.json({ ok: true, - ...(profiles ? { profiles } : {}), - ...(defaults ? { defaults } : {}), + requestQueue: nextResilience.requestQueue, + connectionCooldown: nextResilience.connectionCooldown, + providerBreaker: nextResilience.providerBreaker, + waitForCooldown: { + enabled: nextResilience.waitForCooldown.enabled, + maxRetries: nextResilience.waitForCooldown.maxRetries, + maxRetryWaitSec: nextResilience.waitForCooldown.maxRetryWaitSec, + }, + legacy: buildLegacyResilienceCompat(nextResilience), }); } catch (err: unknown) { console.error("[API] PATCH /api/resilience error:", err); diff --git a/src/app/api/settings/combo-defaults/route.ts b/src/app/api/settings/combo-defaults/route.ts index fb2df51cda..49c01285b4 100644 --- a/src/app/api/settings/combo-defaults/route.ts +++ b/src/app/api/settings/combo-defaults/route.ts @@ -3,6 +3,32 @@ import { getSettings, updateSettings } from "@/lib/localDb"; import { updateComboDefaultsSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +const LEGACY_COMBO_RESILIENCE_KEYS = new Set([ + "timeoutMs", + "healthCheckEnabled", + "healthCheckTimeoutMs", +]); + +function sanitizeComboRuntimeConfig(config?: Record | null) { + if (!config || typeof config !== "object") return {}; + return Object.fromEntries( + Object.entries(config).filter( + ([key, value]) => + value !== undefined && value !== null && !LEGACY_COMBO_RESILIENCE_KEYS.has(key) + ) + ); +} + +function sanitizeProviderOverrides(overrides?: Record | null) { + if (!overrides || typeof overrides !== "object") return {}; + return Object.fromEntries( + Object.entries(overrides).map(([providerId, config]) => [ + providerId, + sanitizeComboRuntimeConfig(config), + ]) + ); +} + /** * GET /api/settings/combo-defaults * Returns the current combo global defaults and provider overrides @@ -10,21 +36,23 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; export async function GET() { try { const settings: any = await getSettings(); + const comboDefaults = sanitizeComboRuntimeConfig(settings.comboDefaults); + const providerOverrides = sanitizeProviderOverrides(settings.providerOverrides); return NextResponse.json({ - comboDefaults: settings.comboDefaults || { - strategy: "priority", - maxRetries: 1, - retryDelayMs: 2000, - timeoutMs: 120000, - healthCheckEnabled: true, - healthCheckTimeoutMs: 3000, - handoffThreshold: 0.85, - handoffModel: "", - maxMessagesForSummary: 30, - maxComboDepth: 3, - trackMetrics: true, - }, - providerOverrides: settings.providerOverrides || {}, + comboDefaults: + Object.keys(comboDefaults).length > 0 + ? comboDefaults + : { + strategy: "priority", + maxRetries: 1, + retryDelayMs: 2000, + handoffThreshold: 0.85, + handoffModel: "", + maxMessagesForSummary: 30, + maxComboDepth: 3, + trackMetrics: true, + }, + providerOverrides, }); } catch (error) { console.log("Error fetching combo defaults:", error); @@ -63,16 +91,16 @@ export async function PATCH(request) { const updates: Record = {}; if (body.comboDefaults) { - updates.comboDefaults = body.comboDefaults; + updates.comboDefaults = sanitizeComboRuntimeConfig(body.comboDefaults); } if (body.providerOverrides) { - updates.providerOverrides = body.providerOverrides; + updates.providerOverrides = sanitizeProviderOverrides(body.providerOverrides); } const settings: any = await updateSettings(updates); return NextResponse.json({ - comboDefaults: settings.comboDefaults || {}, - providerOverrides: settings.providerOverrides || {}, + comboDefaults: sanitizeComboRuntimeConfig(settings.comboDefaults), + providerOverrides: sanitizeProviderOverrides(settings.providerOverrides), }); } catch (error) { console.log("Error updating combo defaults:", error); diff --git a/src/domain/modelAvailability.ts b/src/domain/modelAvailability.ts deleted file mode 100644 index 9510c0ea73..0000000000 --- a/src/domain/modelAvailability.ts +++ /dev/null @@ -1,319 +0,0 @@ -/** - * Model Availability — Domain Layer (T-19) - * - * Tracks model availability per provider with TTL-based cooldowns. - * When a model becomes unavailable (rate-limited, erroring), it is - * marked with a cooldown period. The availability report powers - * the dashboard health view. - * - * @module domain/modelAvailability - */ - -/** - * @typedef {Object} UnavailableEntry - * @property {string} provider - * @property {string} model - * @property {number} unavailableSince - timestamp - * @property {number} cooldownMs - * @property {string} [reason] - */ - -/** @type {Map} */ -const unavailable = new Map(); - -/** - * @typedef {Object} FailureState - * @property {number} failureCount - * @property {number} lastFailureAt - * @property {number} resetAfterMs - */ - -/** - * @typedef {Object} ProviderProfile - * @property {number} [transientCooldown] - * @property {number} [rateLimitCooldown] - * @property {number} [maxBackoffLevel] - * @property {number} [circuitBreakerThreshold] - * @property {number} [circuitBreakerReset] - */ - -/** @type {Map} */ -const failureState = new Map(); - -const FAILURE_WINDOW_MS = 30 * 60 * 1000; - -const PROBLEMATIC_STATUS_COOLDOWNS = { - 429: 5 * 60 * 1000, - 408: 60 * 1000, - 500: 2 * 60 * 1000, - 502: 2 * 60 * 1000, - 503: 2 * 60 * 1000, - 504: 2 * 60 * 1000, -}; - -const MIN_PROBLEMATIC_COOLDOWN_MS = 60 * 1000; -const MAX_PROBLEMATIC_COOLDOWN_MS = 30 * 60 * 1000; - -function toPositiveNumber(value) { - return Number.isFinite(value) && Number(value) > 0 ? Number(value) : null; -} - -function toNonNegativeNumber(value) { - return Number.isFinite(value) && Number(value) >= 0 ? Number(value) : null; -} - -/** - * The first layer already reacts immediately to authoritative model/account failures. - * Global provider/model quarantine is the escalation layer, so its failure window and - * threshold are only customized when a runtime provider profile is supplied. - * - * @param {ProviderProfile | null | undefined} profile - * @returns {number} - */ -function getFailureWindowMs(profile) { - return toPositiveNumber(profile?.circuitBreakerReset) ?? FAILURE_WINDOW_MS; -} - -/** - * Without a runtime profile we preserve legacy behavior: quarantine on the first failure. - * - * @param {ProviderProfile | null | undefined} profile - * @returns {number} - */ -function getFailureThreshold(profile) { - return toPositiveNumber(profile?.circuitBreakerThreshold) ?? 1; -} - -function getLegacyStatusCooldown(status) { - return status && Object.prototype.hasOwnProperty.call(PROBLEMATIC_STATUS_COOLDOWNS, status) - ? PROBLEMATIC_STATUS_COOLDOWNS[status] - : 0; -} - -/** - * @param {number | null} status - * @param {ProviderProfile | null | undefined} profile - * @returns {number} - */ -function getProfileStatusCooldown(status, profile) { - if (!profile) return 0; - if (status === 429) { - return toPositiveNumber(profile.rateLimitCooldown) ?? 0; - } - return toPositiveNumber(profile.transientCooldown) ?? 0; -} - -/** - * @param {number} baseCooldownMs - * @param {number} failureCount - * @param {ProviderProfile | null | undefined} profile - * @returns {number} - */ -function getScaledCooldown(baseCooldownMs, failureCount, profile) { - const safeBase = toPositiveNumber(baseCooldownMs) ?? 1000; - if (!profile) { - return Math.min( - Math.max(safeBase, MIN_PROBLEMATIC_COOLDOWN_MS) * Math.pow(2, Math.max(0, failureCount - 1)), - MAX_PROBLEMATIC_COOLDOWN_MS - ); - } - - const maxBackoffLevel = Math.max( - 0, - Math.trunc(toNonNegativeNumber(profile.maxBackoffLevel) ?? 0) - ); - const exponent = Math.min(Math.max(0, failureCount - 1), maxBackoffLevel); - return safeBase * Math.pow(2, exponent); -} - -/** - * Build a composite key for provider+model. - * @param {string} provider - * @param {string} model - * @returns {string} - */ -function makeKey(provider, model) { - return `${provider}::${model}`; -} - -/** - * Check if a model is currently available. - * - * @param {string} provider - Provider ID (e.g. "openai", "anthropic") - * @param {string} model - Model ID (e.g. "gpt-4o", "claude-sonnet-4-20250514") - * @returns {boolean} true if model is available (not in cooldown) - */ -export function isModelAvailable(provider, model) { - const key = makeKey(provider, model); - const entry = unavailable.get(key); - if (!entry) return true; - - // Check if cooldown has expired - if (Date.now() - entry.unavailableSince >= entry.cooldownMs) { - unavailable.delete(key); - return true; - } - - return false; -} - -/** - * Get remaining cooldown information for a model, if it is currently unavailable. - * - * @param {string} provider - * @param {string} model - * @returns {{ provider: string, model: string, reason: string, remainingMs: number, unavailableSince: string } | null} - */ -export function getModelCooldownInfo(provider, model) { - const key = makeKey(provider, model); - const entry = unavailable.get(key); - if (!entry) return null; - - const elapsed = Date.now() - entry.unavailableSince; - if (elapsed >= entry.cooldownMs) { - unavailable.delete(key); - return null; - } - - return { - provider: entry.provider, - model: entry.model, - reason: entry.reason || "unknown", - remainingMs: entry.cooldownMs - elapsed, - unavailableSince: new Date(entry.unavailableSince).toISOString(), - }; -} - -/** - * Mark a model as temporarily unavailable. - * - * @param {string} provider - * @param {string} model - * @param {number} [cooldownMs=60000] - Cooldown in milliseconds (default 60s) - * @param {string} [reason] - Optional reason for unavailability - */ -export function setModelUnavailable(provider, model, cooldownMs = 60000, reason) { - const key = makeKey(provider, model); - const now = Date.now(); - const safeCooldownMs = Number.isFinite(cooldownMs) && cooldownMs > 0 ? cooldownMs : 60000; - const existing = unavailable.get(key); - const existingRemainingMs = - existing && Date.now() - existing.unavailableSince < existing.cooldownMs - ? existing.cooldownMs - (Date.now() - existing.unavailableSince) - : 0; - const effectiveCooldownMs = Math.max(safeCooldownMs, existingRemainingMs); - - unavailable.set(key, { - provider, - model, - unavailableSince: now, - cooldownMs: effectiveCooldownMs, - reason: reason || "unknown", - }); -} - -/** - * Marca provider/model como problemático com cooldown adaptativo. - * Mantém retrocompatibilidade: não altera o comportamento de setModelUnavailable, - * apenas oferece uma estratégia mais agressiva para falhas recorrentes. - * - * @param {string} provider - * @param {string} model - * @param {{ status?: number, baseCooldownMs?: number, reason?: string, profile?: ProviderProfile | null }} [options] - * @returns {{ cooldownMs: number, failureCount: number, quarantined: boolean, threshold: number, resetAfterMs: number }} - */ -export function markModelAsProblematic(provider, model, options = {}) { - const key = makeKey(provider, model); - const now = Date.now(); - const status = Number.isFinite(options.status) ? Number(options.status) : null; - const profile = options.profile || null; - const explicitBaseCooldownMs = - Number.isFinite(options.baseCooldownMs) && Number(options.baseCooldownMs) > 0 - ? Number(options.baseCooldownMs) - : 0; - const statusBaseCooldown = profile - ? getProfileStatusCooldown(status, profile) - : getLegacyStatusCooldown(status); - const baseCooldownMs = Math.max(explicitBaseCooldownMs, statusBaseCooldown); - - const prev = failureState.get(key); - const resetAfterMs = getFailureWindowMs(profile); - const withinFailureWindow = prev && now - prev.lastFailureAt <= prev.resetAfterMs; - const failureCount = withinFailureWindow ? prev.failureCount + 1 : 1; - failureState.set(key, { failureCount, lastFailureAt: now, resetAfterMs }); - - const threshold = getFailureThreshold(profile); - const cooldownMs = getScaledCooldown(baseCooldownMs, failureCount, profile); - const quarantined = failureCount >= threshold; - - if (quarantined) { - setModelUnavailable(provider, model, cooldownMs, options.reason || "problematic_model"); - } - - return { - cooldownMs, - failureCount, - quarantined, - threshold, - resetAfterMs, - }; -} - -/** - * Clear unavailability for a model (e.g. after manual reset). - * - * @param {string} provider - * @param {string} model - * @returns {boolean} true if entry existed and was removed - */ -export function clearModelUnavailability(provider, model) { - const key = makeKey(provider, model); - failureState.delete(key); - return unavailable.delete(key); -} - -/** - * Get a report of all currently unavailable models. - * - * @returns {Array<{ provider: string, model: string, reason: string, remainingMs: number, unavailableSince: string }>} - */ -export function getAvailabilityReport() { - const now = Date.now(); - const report = []; - - for (const [key, entry] of unavailable.entries()) { - const elapsed = now - entry.unavailableSince; - if (elapsed >= entry.cooldownMs) { - unavailable.delete(key); - continue; - } - - report.push({ - provider: entry.provider, - model: entry.model, - reason: entry.reason || "unknown", - remainingMs: entry.cooldownMs - elapsed, - unavailableSince: new Date(entry.unavailableSince).toISOString(), - }); - } - - return report; -} - -/** - * Get total count of unavailable models. - * @returns {number} - */ -export function getUnavailableCount() { - // Prune expired entries first - getAvailabilityReport(); - return unavailable.size; -} - -/** - * Reset all availability states (for testing or admin). - */ -export function resetAllAvailability() { - unavailable.clear(); - failureState.clear(); -} diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 9e1726d107..d0f1202c61 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -282,6 +282,10 @@ "evals": "Evals", "utilization": "Utilization", "utilizationDescription": "Provider quota usage trends and rate limit tracking", + "modelStatus": "Model Status", + "modelStatusCooldown": "Cooldown", + "modelStatusUnavailable": "Unavailable", + "modelStatusError": "Error", "comboHealth": "Combo Health", "comboHealthDescription": "Combo-level quota, usage distribution, and performance metrics" }, @@ -644,7 +648,8 @@ "kiro": "Use when integrating Kiro and controlling model routing centrally from OmniRoute.", "windsurf": "Use when you want an AI-first IDE with Codeium/Windsurf models routed through OmniRoute.", "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", - "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules." + "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", + "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks." }, "toolDescriptions": { "antigravity": "Google Antigravity IDE with MITM", @@ -1108,6 +1113,7 @@ "builderAccount": "Account", "builderPreview": "Preview", "builderAddStep": "Add step", + "builderDuplicateExact": "This exact provider/model/account step is already in the combo.", "builderComboRef": "Combo Ref", "builderAddComboRef": "Add combo reference", "builderComboRefStep": "Add combo reference", @@ -1865,6 +1871,9 @@ "compatUpstreamAddRow": "Add header", "compatUpstreamRemoveRow": "Remove row", "compatBadgeUpstreamHeaders": "Headers", + "perModelQuotaLabel": "Per-Model Quota", + "perModelQuotaDescription": "When enabled, 429/404 errors only lock the specific model, not the entire connection. Use for providers with per-model rate limits (e.g., ModelScope).", + "perModelQuotaToggle": "Per-Model Quota Toggle", "modelId": "Model ID", "customModelPlaceholder": "e.g. gpt-4.5-turbo", "loading": "Loading...", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index f2e7706f0c..5f6f7e9655 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -282,6 +282,10 @@ "evals": "评测", "utilization": "利用率", "utilizationDescription": "提供商配额使用趋势和速率限制跟踪", + "modelStatus": "模型状态", + "modelStatusCooldown": "冷却中", + "modelStatusUnavailable": "不可用", + "modelStatusError": "错误", "comboHealth": "组合健康状况", "comboHealthDescription": "组合级别配额、使用分布和性能指标" }, @@ -644,7 +648,8 @@ "kiro": "在集成 Kiro 并从 OmniRoute 集中控制模型路由时使用。", "windsurf": "当您需要 Windsurf AI IDE 并通过 OmniRoute 路由模型时使用。", "antigravity": "当必须通过 MITM 拦截 Antigravity/Kiro 流量并将其路由到 OmniRoute 时使用。", - "copilot": "当您想要 Copilot 聊天风格的 UX 同时强制执行 OmniRoute 键和路由规则时使用。" + "copilot": "当您想要 Copilot 聊天风格的 UX 同时强制执行 OmniRoute 键和路由规则时使用。", + "qwen": "当您需要使用阿里云 Qwen Code CLI 进行编码任务时使用。" }, "toolDescriptions": { "antigravity": "带 MITM 的 Google Antigravity IDE", @@ -1022,7 +1027,8 @@ "selectAccount": "选择账户", "autoSelectAccount": "运行时自动选择账户", "previewNextStep": "选择提供商和模型以预览下一步。", - "addStep": "添加步骤", + "builderAddStep": "添加步骤", + "builderDuplicateExact": "该提供商/模型/账户步骤已存在于组合中。", "comboReference": "组合引用", "selectComboToReference": "选择要引用的现有组合", "addComboReference": "添加组合引用", @@ -1911,6 +1917,9 @@ "compatUpstreamAddRow": "添加请求头", "compatUpstreamRemoveRow": "删除此行", "compatBadgeUpstreamHeaders": "请求头", + "perModelQuotaLabel": "按模型配额", + "perModelQuotaDescription": "启用后,429/404错误只会锁定特定模型,而不是整个连接。适用于具有按模型速率限制的提供商(例如ModelScope)。", + "perModelQuotaToggle": "按模型配额开关", "modelId": "模型 ID", "customModelPlaceholder": "例如:gpt-4.5-turbo", "loading": "正在加载...", diff --git a/src/lib/combos/intelligentRouting.ts b/src/lib/combos/intelligentRouting.ts index 1f2a285a6a..7f42f5a0cb 100644 --- a/src/lib/combos/intelligentRouting.ts +++ b/src/lib/combos/intelligentRouting.ts @@ -31,13 +31,6 @@ export type IntelligentProviderScore = { factors: IntelligentRoutingWeights; }; -export type IntelligentExclusionEntry = { - provider: string; - excludedAt: string; - cooldownMs: number; - reason: string; -}; - export const DEFAULT_INTELLIGENT_WEIGHTS: IntelligentRoutingWeights = { quota: 0.2, health: 0.25, @@ -161,50 +154,3 @@ export function buildIntelligentProviderScores(combo: { factors: weights, })); } - -export function extractIntelligentHealthState(health: unknown): { - incidentMode: boolean; - exclusions: IntelligentExclusionEntry[]; -} { - const healthRecord = isRecord(health) ? health : {}; - const providerHealth = isRecord(healthRecord.providerHealth) ? healthRecord.providerHealth : {}; - const providerBreakers = Object.entries(providerHealth).map(([provider, status]) => { - const statusRecord = isRecord(status) ? status : {}; - return { - provider, - state: typeof statusRecord.state === "string" ? statusRecord.state : "CLOSED", - lastFailure: typeof statusRecord.lastFailure === "string" ? statusRecord.lastFailure : null, - }; - }); - const breakersFromArray = Array.isArray(healthRecord.circuitBreakers) - ? healthRecord.circuitBreakers - .map((entry) => { - const breaker = isRecord(entry) ? entry : {}; - const provider = - typeof breaker.provider === "string" - ? breaker.provider - : typeof breaker.name === "string" - ? breaker.name - : "unknown"; - return { - provider, - state: typeof breaker.state === "string" ? breaker.state : "CLOSED", - lastFailure: typeof breaker.lastFailure === "string" ? breaker.lastFailure : null, - }; - }) - .filter((entry) => typeof entry.provider === "string") - : []; - - const breakers = breakersFromArray.length > 0 ? breakersFromArray : providerBreakers; - const openBreakers = breakers.filter((breaker) => breaker.state === "OPEN"); - - return { - incidentMode: openBreakers.length / Math.max(breakers.length, 1) > 0.5, - exclusions: openBreakers.map((breaker) => ({ - provider: breaker.provider, - excludedAt: breaker.lastFailure || new Date().toISOString(), - cooldownMs: 5 * 60 * 1000, - reason: "Circuit breaker OPEN", - })), - }; -} diff --git a/src/lib/dataPaths.js b/src/lib/dataPaths.js deleted file mode 100644 index 3595429936..0000000000 --- a/src/lib/dataPaths.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.APP_NAME = void 0; -exports.getLegacyDotDataDir = getLegacyDotDataDir; -exports.getDefaultDataDir = getDefaultDataDir; -exports.resolveDataDir = resolveDataDir; -exports.isSamePath = isSamePath; -const path_1 = __importDefault(require("path")); -const os_1 = __importDefault(require("os")); -exports.APP_NAME = "omniroute"; -function fallbackHomeDir() { - const envHome = process.env.HOME || process.env.USERPROFILE; - if (typeof envHome === "string" && envHome.trim().length > 0) { - return path_1.default.resolve(envHome); - } - return os_1.default.tmpdir(); -} -function safeHomeDir() { - try { - return os_1.default.homedir(); - } - catch { - return fallbackHomeDir(); - } -} -function normalizeConfiguredPath(dir) { - if (typeof dir !== "string") - return null; - const trimmed = dir.trim(); - if (!trimmed) - return null; - return path_1.default.resolve(trimmed); -} -function getLegacyDotDataDir() { - return path_1.default.join(safeHomeDir(), `.${exports.APP_NAME}`); -} -function getDefaultDataDir() { - const homeDir = safeHomeDir(); - if (process.platform === "win32") { - const appData = process.env.APPDATA || path_1.default.join(homeDir, "AppData", "Roaming"); - return path_1.default.join(appData, exports.APP_NAME); - } - // Support XDG on Linux/macOS when explicitly configured. - const xdgConfigHome = normalizeConfiguredPath(process.env.XDG_CONFIG_HOME); - if (xdgConfigHome) { - return path_1.default.join(xdgConfigHome, exports.APP_NAME); - } - return getLegacyDotDataDir(); -} -function resolveDataDir({ isCloud = false } = {}) { - if (isCloud) - return "/tmp"; - const configured = normalizeConfiguredPath(process.env.DATA_DIR); - if (configured) - return configured; - return getDefaultDataDir(); -} -function isSamePath(a, b) { - if (!a || !b) - return false; - const normalizedA = path_1.default.resolve(a); - const normalizedB = path_1.default.resolve(b); - if (process.platform === "win32") { - return normalizedA.toLowerCase() === normalizedB.toLowerCase(); - } - return normalizedA === normalizedB; -} diff --git a/src/lib/monitoring/observability.ts b/src/lib/monitoring/observability.ts index 8feb1cdcc7..0088fd5192 100644 --- a/src/lib/monitoring/observability.ts +++ b/src/lib/monitoring/observability.ts @@ -4,7 +4,8 @@ interface CircuitBreakerStatus { name: string; state: string; failureCount?: number; - lastFailureTime?: string | null; + lastFailureTime?: number | string | null; + retryAfterMs?: number; } interface SessionSnapshot { @@ -155,13 +156,31 @@ export function buildHealthPayload({ platform: process.platform, }; + const providerBreakers = circuitBreakers + .filter((cb) => !cb.name.startsWith("test-") && !cb.name.startsWith("test_")) + .map((cb) => { + const lastFailure = + typeof cb.lastFailureTime === "number" && Number.isFinite(cb.lastFailureTime) + ? new Date(cb.lastFailureTime).toISOString() + : typeof cb.lastFailureTime === "string" + ? cb.lastFailureTime + : null; + return { + provider: cb.name, + state: cb.state, + failureCount: cb.failureCount || 0, + lastFailure, + retryAfterMs: cb.retryAfterMs || 0, + }; + }); + const providerHealth: Record = {}; - for (const cb of circuitBreakers) { - if (cb.name.startsWith("test-") || cb.name.startsWith("test_")) continue; - providerHealth[cb.name] = { - state: cb.state, - failures: cb.failureCount || 0, - lastFailure: cb.lastFailureTime || null, + for (const breaker of providerBreakers) { + providerHealth[breaker.provider] = { + state: breaker.state, + failures: breaker.failureCount, + lastFailure: breaker.lastFailure, + retryAfterMs: breaker.retryAfterMs, }; } @@ -197,6 +216,7 @@ export function buildHealthPayload({ ...breakerCounts, total: breakerCounts.open + breakerCounts.halfOpen + breakerCounts.closed, }, + providerBreakers, providerHealth, providerSummary: { catalogCount, diff --git a/src/lib/resilience/settings.ts b/src/lib/resilience/settings.ts new file mode 100644 index 0000000000..f6f65e8faf --- /dev/null +++ b/src/lib/resilience/settings.ts @@ -0,0 +1,419 @@ +import { DEFAULT_API_LIMITS, PROVIDER_PROFILES } from "@omniroute/open-sse/config/constants"; + +type JsonRecord = Record; +type AuthCategory = "oauth" | "apikey"; + +export interface RequestQueueSettings { + autoEnableApiKeyProviders: boolean; + requestsPerMinute: number; + minTimeBetweenRequestsMs: number; + concurrentRequests: number; + maxWaitMs: number; +} + +export interface ConnectionCooldownProfileSettings { + baseCooldownMs: number; + useUpstreamRetryHints: boolean; + maxBackoffSteps: number; +} + +export interface ProviderBreakerProfileSettings { + failureThreshold: number; + resetTimeoutMs: number; +} + +export interface WaitForCooldownSettings { + enabled: boolean; + maxRetries: number; + maxRetryWaitSec: number; + maxRetryWaitMs: number; +} + +export interface ResilienceSettings { + requestQueue: RequestQueueSettings; + connectionCooldown: Record; + providerBreaker: Record; + waitForCooldown: WaitForCooldownSettings; +} + +export interface ResilienceSettingsPatch { + requestQueue?: Partial; + connectionCooldown?: Partial>>; + providerBreaker?: Partial>>; + waitForCooldown?: Partial; +} + +function asRecord(value: unknown): JsonRecord { + return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; +} + +function toInteger( + value: unknown, + fallback: number, + options: { min?: number; max?: number } = {} +): number { + const min = options.min ?? 0; + const max = options.max ?? Number.MAX_SAFE_INTEGER; + const parsed = + typeof value === "number" + ? value + : typeof value === "string" && value.trim().length > 0 + ? Number(value) + : Number.NaN; + + if (!Number.isFinite(parsed)) { + return fallback; + } + + return Math.min(max, Math.max(min, Math.trunc(parsed))); +} + +function toBoolean(value: unknown, fallback: boolean): boolean { + return typeof value === "boolean" ? value : fallback; +} + +export const DEFAULT_REQUEST_QUEUE_MAX_WAIT_MS = (() => { + const parsed = Number(process.env.RATE_LIMIT_MAX_WAIT_MS || "120000"); + return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : 120000; +})(); + +export const DEFAULT_RESILIENCE_SETTINGS: ResilienceSettings = { + requestQueue: { + autoEnableApiKeyProviders: true, + requestsPerMinute: DEFAULT_API_LIMITS.requestsPerMinute, + minTimeBetweenRequestsMs: DEFAULT_API_LIMITS.minTimeBetweenRequests, + concurrentRequests: DEFAULT_API_LIMITS.concurrentRequests, + maxWaitMs: DEFAULT_REQUEST_QUEUE_MAX_WAIT_MS, + }, + connectionCooldown: { + oauth: { + baseCooldownMs: PROVIDER_PROFILES.oauth.transientCooldown, + useUpstreamRetryHints: PROVIDER_PROFILES.oauth.rateLimitCooldown === 0, + maxBackoffSteps: PROVIDER_PROFILES.oauth.maxBackoffLevel, + }, + apikey: { + baseCooldownMs: PROVIDER_PROFILES.apikey.transientCooldown, + useUpstreamRetryHints: PROVIDER_PROFILES.apikey.rateLimitCooldown === 0, + maxBackoffSteps: PROVIDER_PROFILES.apikey.maxBackoffLevel, + }, + }, + providerBreaker: { + oauth: { + failureThreshold: PROVIDER_PROFILES.oauth.circuitBreakerThreshold, + resetTimeoutMs: PROVIDER_PROFILES.oauth.circuitBreakerReset, + }, + apikey: { + failureThreshold: PROVIDER_PROFILES.apikey.circuitBreakerThreshold, + resetTimeoutMs: PROVIDER_PROFILES.apikey.circuitBreakerReset, + }, + }, + waitForCooldown: { + enabled: true, + maxRetries: 3, + maxRetryWaitSec: 30, + maxRetryWaitMs: 30000, + }, +}; + +function normalizeRequestQueueSettings( + next: unknown, + fallback: RequestQueueSettings +): RequestQueueSettings { + const record = asRecord(next); + const requestsPerMinute = toInteger(record.requestsPerMinute, fallback.requestsPerMinute, { + min: 1, + max: 1_000_000, + }); + const minTimeBetweenRequestsMs = toInteger( + record.minTimeBetweenRequestsMs, + fallback.minTimeBetweenRequestsMs, + { min: 0, max: 60 * 60 * 1000 } + ); + const concurrentRequests = toInteger(record.concurrentRequests, fallback.concurrentRequests, { + min: 1, + max: 10_000, + }); + const maxWaitMs = toInteger(record.maxWaitMs, fallback.maxWaitMs, { + min: 1, + max: 24 * 60 * 60 * 1000, + }); + + return { + autoEnableApiKeyProviders: toBoolean( + record.autoEnableApiKeyProviders, + fallback.autoEnableApiKeyProviders + ), + requestsPerMinute, + minTimeBetweenRequestsMs, + concurrentRequests, + maxWaitMs, + }; +} + +function normalizeConnectionCooldownProfile( + next: unknown, + fallback: ConnectionCooldownProfileSettings +): ConnectionCooldownProfileSettings { + const record = asRecord(next); + return { + baseCooldownMs: toInteger(record.baseCooldownMs, fallback.baseCooldownMs, { + min: 0, + max: 24 * 60 * 60 * 1000, + }), + useUpstreamRetryHints: toBoolean(record.useUpstreamRetryHints, fallback.useUpstreamRetryHints), + maxBackoffSteps: toInteger(record.maxBackoffSteps, fallback.maxBackoffSteps, { + min: 0, + max: 32, + }), + }; +} + +function normalizeLegacyConnectionCooldownProfile( + next: unknown, + fallback: ConnectionCooldownProfileSettings +): ConnectionCooldownProfileSettings { + const record = asRecord(next); + const transientCooldown = toInteger(record.transientCooldown, fallback.baseCooldownMs, { + min: 0, + max: 24 * 60 * 60 * 1000, + }); + const legacyRateLimitCooldown = toInteger(record.rateLimitCooldown, transientCooldown, { + min: 0, + max: 24 * 60 * 60 * 1000, + }); + const useUpstreamRetryHints = + typeof record.rateLimitCooldown === "number" + ? record.rateLimitCooldown === 0 + : fallback.useUpstreamRetryHints; + + return { + baseCooldownMs: useUpstreamRetryHints + ? transientCooldown + : Math.max(transientCooldown, legacyRateLimitCooldown), + useUpstreamRetryHints, + maxBackoffSteps: toInteger(record.maxBackoffLevel, fallback.maxBackoffSteps, { + min: 0, + max: 32, + }), + }; +} + +function normalizeProviderBreakerProfile( + next: unknown, + fallback: ProviderBreakerProfileSettings +): ProviderBreakerProfileSettings { + const record = asRecord(next); + return { + failureThreshold: toInteger(record.failureThreshold, fallback.failureThreshold, { + min: 1, + max: 1000, + }), + resetTimeoutMs: toInteger(record.resetTimeoutMs, fallback.resetTimeoutMs, { + min: 1000, + max: 24 * 60 * 60 * 1000, + }), + }; +} + +function normalizeWaitForCooldownSettings( + next: unknown, + fallback: WaitForCooldownSettings +): WaitForCooldownSettings { + const record = asRecord(next); + const maxRetryWaitSec = toInteger(record.maxRetryWaitSec, fallback.maxRetryWaitSec, { + min: 0, + max: 300, + }); + const maxRetries = toInteger(record.maxRetries, fallback.maxRetries, { min: 0, max: 10 }); + const enabled = + toBoolean(record.enabled, fallback.enabled) && maxRetries > 0 && maxRetryWaitSec > 0; + + return { + enabled, + maxRetries, + maxRetryWaitSec, + maxRetryWaitMs: maxRetryWaitSec * 1000, + }; +} + +function buildLegacyFallback(settings: JsonRecord): ResilienceSettings { + const profiles = asRecord(settings.providerProfiles); + const defaults = asRecord(settings.rateLimitDefaults); + + const oauthLegacy = asRecord(profiles.oauth); + const apikeyLegacy = asRecord(profiles.apikey); + + const waitMaxRetrySec = toInteger( + settings.maxRetryIntervalSec, + DEFAULT_RESILIENCE_SETTINGS.waitForCooldown.maxRetryWaitSec, + { min: 0, max: 300 } + ); + const waitMaxRetries = toInteger( + settings.requestRetry, + DEFAULT_RESILIENCE_SETTINGS.waitForCooldown.maxRetries, + { min: 0, max: 10 } + ); + + return { + requestQueue: { + autoEnableApiKeyProviders: DEFAULT_RESILIENCE_SETTINGS.requestQueue.autoEnableApiKeyProviders, + requestsPerMinute: toInteger( + defaults.requestsPerMinute, + DEFAULT_RESILIENCE_SETTINGS.requestQueue.requestsPerMinute, + { min: 1, max: 1_000_000 } + ), + minTimeBetweenRequestsMs: toInteger( + defaults.minTimeBetweenRequests, + DEFAULT_RESILIENCE_SETTINGS.requestQueue.minTimeBetweenRequestsMs, + { min: 0, max: 60 * 60 * 1000 } + ), + concurrentRequests: toInteger( + defaults.concurrentRequests, + DEFAULT_RESILIENCE_SETTINGS.requestQueue.concurrentRequests, + { min: 1, max: 10_000 } + ), + maxWaitMs: DEFAULT_RESILIENCE_SETTINGS.requestQueue.maxWaitMs, + }, + connectionCooldown: { + oauth: normalizeLegacyConnectionCooldownProfile( + oauthLegacy, + DEFAULT_RESILIENCE_SETTINGS.connectionCooldown.oauth + ), + apikey: normalizeLegacyConnectionCooldownProfile( + apikeyLegacy, + DEFAULT_RESILIENCE_SETTINGS.connectionCooldown.apikey + ), + }, + providerBreaker: { + oauth: { + failureThreshold: toInteger( + oauthLegacy.circuitBreakerThreshold, + DEFAULT_RESILIENCE_SETTINGS.providerBreaker.oauth.failureThreshold, + { min: 1, max: 1000 } + ), + resetTimeoutMs: toInteger( + oauthLegacy.circuitBreakerReset, + DEFAULT_RESILIENCE_SETTINGS.providerBreaker.oauth.resetTimeoutMs, + { min: 1000, max: 24 * 60 * 60 * 1000 } + ), + }, + apikey: { + failureThreshold: toInteger( + apikeyLegacy.circuitBreakerThreshold, + DEFAULT_RESILIENCE_SETTINGS.providerBreaker.apikey.failureThreshold, + { min: 1, max: 1000 } + ), + resetTimeoutMs: toInteger( + apikeyLegacy.circuitBreakerReset, + DEFAULT_RESILIENCE_SETTINGS.providerBreaker.apikey.resetTimeoutMs, + { min: 1000, max: 24 * 60 * 60 * 1000 } + ), + }, + }, + waitForCooldown: { + enabled: waitMaxRetries > 0 && waitMaxRetrySec > 0, + maxRetries: waitMaxRetries, + maxRetryWaitSec: waitMaxRetrySec, + maxRetryWaitMs: waitMaxRetrySec * 1000, + }, + }; +} + +export function resolveResilienceSettings( + settings: Record | null | undefined +): ResilienceSettings { + const record = asRecord(settings); + const current = asRecord(record.resilienceSettings); + const fallback = buildLegacyFallback(record); + + return { + requestQueue: normalizeRequestQueueSettings(current.requestQueue, fallback.requestQueue), + connectionCooldown: { + oauth: normalizeConnectionCooldownProfile( + asRecord(current.connectionCooldown).oauth, + fallback.connectionCooldown.oauth + ), + apikey: normalizeConnectionCooldownProfile( + asRecord(current.connectionCooldown).apikey, + fallback.connectionCooldown.apikey + ), + }, + providerBreaker: { + oauth: normalizeProviderBreakerProfile( + asRecord(current.providerBreaker).oauth, + fallback.providerBreaker.oauth + ), + apikey: normalizeProviderBreakerProfile( + asRecord(current.providerBreaker).apikey, + fallback.providerBreaker.apikey + ), + }, + waitForCooldown: normalizeWaitForCooldownSettings( + current.waitForCooldown, + fallback.waitForCooldown + ), + }; +} + +export function mergeResilienceSettings( + current: ResilienceSettings, + updates: ResilienceSettingsPatch +): ResilienceSettings { + return { + requestQueue: normalizeRequestQueueSettings(updates.requestQueue, current.requestQueue), + connectionCooldown: { + oauth: normalizeConnectionCooldownProfile( + updates.connectionCooldown?.oauth, + current.connectionCooldown.oauth + ), + apikey: normalizeConnectionCooldownProfile( + updates.connectionCooldown?.apikey, + current.connectionCooldown.apikey + ), + }, + providerBreaker: { + oauth: normalizeProviderBreakerProfile( + updates.providerBreaker?.oauth, + current.providerBreaker.oauth + ), + apikey: normalizeProviderBreakerProfile( + updates.providerBreaker?.apikey, + current.providerBreaker.apikey + ), + }, + waitForCooldown: normalizeWaitForCooldownSettings( + updates.waitForCooldown, + current.waitForCooldown + ), + }; +} + +export function buildLegacyResilienceCompat(settings: ResilienceSettings) { + return { + profiles: { + oauth: { + transientCooldown: settings.connectionCooldown.oauth.baseCooldownMs, + rateLimitCooldown: settings.connectionCooldown.oauth.useUpstreamRetryHints + ? 0 + : settings.connectionCooldown.oauth.baseCooldownMs, + maxBackoffLevel: settings.connectionCooldown.oauth.maxBackoffSteps, + circuitBreakerThreshold: settings.providerBreaker.oauth.failureThreshold, + circuitBreakerReset: settings.providerBreaker.oauth.resetTimeoutMs, + }, + apikey: { + transientCooldown: settings.connectionCooldown.apikey.baseCooldownMs, + rateLimitCooldown: settings.connectionCooldown.apikey.useUpstreamRetryHints + ? 0 + : settings.connectionCooldown.apikey.baseCooldownMs, + maxBackoffLevel: settings.connectionCooldown.apikey.maxBackoffSteps, + circuitBreakerThreshold: settings.providerBreaker.apikey.failureThreshold, + circuitBreakerReset: settings.providerBreaker.apikey.resetTimeoutMs, + }, + }, + defaults: { + requestsPerMinute: settings.requestQueue.requestsPerMinute, + minTimeBetweenRequests: settings.requestQueue.minTimeBetweenRequestsMs, + concurrentRequests: settings.requestQueue.concurrentRequests, + }, + }; +} diff --git a/src/shared/utils/circuitBreaker.ts b/src/shared/utils/circuitBreaker.ts index 3f9e0e332a..3700bba31c 100644 --- a/src/shared/utils/circuitBreaker.ts +++ b/src/shared/utils/circuitBreaker.ts @@ -180,6 +180,7 @@ export class CircuitBreaker { state: this.state, failureCount: this.failureCount, lastFailureTime: this.lastFailureTime, + retryAfterMs: this.getRetryAfterMs(), }; } diff --git a/src/shared/utils/clipboard.ts b/src/shared/utils/clipboard.ts index ee6d014dfb..50f6671dc9 100644 --- a/src/shared/utils/clipboard.ts +++ b/src/shared/utils/clipboard.ts @@ -11,13 +11,10 @@ * @returns true if copy succeeded, false otherwise */ export async function copyToClipboard(text: string): Promise { - // Method 1: Clipboard API (requires HTTPS / secure context) - if ( - typeof navigator !== "undefined" && - navigator.clipboard && - typeof window !== "undefined" && - window.isSecureContext - ) { + // Method 1: Clipboard API + // Works on HTTPS, localhost (treated as secure context), and some browsers + // even on HTTP. Try unconditionally — the catch handles failures. + if (typeof navigator !== "undefined" && navigator.clipboard) { try { await navigator.clipboard.writeText(text); return true; diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index ae120f3da7..fc6d83101e 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -616,11 +616,6 @@ export const updateModelAliasSchema = z.object({ alias: z.string().trim().min(1, "Alias is required").max(200), }); -export const clearModelAvailabilitySchema = z.object({ - provider: z.string().trim().min(1, "provider is required").max(120), - model: modelIdSchema, -}); - /** Align with `sanitizeUpstreamHeadersMap` — allow non-ASCII names; reject Host / hop-by-hop / whitespace / ":". */ const upstreamHeaderNameSchema = z .string() @@ -709,7 +704,7 @@ export const toggleRateLimitSchema = z.object({ enabled: z.boolean(), }); -const resilienceProfileSchema = z.object({ +const legacyResilienceProfileSchema = z.object({ transientCooldown: z.number().min(0), rateLimitCooldown: z.number().min(0), maxBackoffLevel: z.number().int().min(0), @@ -717,30 +712,87 @@ const resilienceProfileSchema = z.object({ circuitBreakerReset: z.number().min(0), }); -const resilienceDefaultsSchema = z +const legacyResilienceDefaultsSchema = z .object({ requestsPerMinute: z.number().int().min(1).optional(), - minTimeBetweenRequests: z.number().int().min(1).optional(), + minTimeBetweenRequests: z.number().int().min(0).optional(), concurrentRequests: z.number().int().min(1).optional(), }) .strict(); +const requestQueueSettingsSchema = z + .object({ + autoEnableApiKeyProviders: z.boolean().optional(), + requestsPerMinute: z.number().int().min(1).optional(), + minTimeBetweenRequestsMs: z.number().int().min(0).optional(), + concurrentRequests: z.number().int().min(1).optional(), + maxWaitMs: z.number().int().min(1).optional(), + }) + .strict(); + +const connectionCooldownProfileSchema = z + .object({ + baseCooldownMs: z.number().int().min(0).optional(), + useUpstreamRetryHints: z.boolean().optional(), + maxBackoffSteps: z.number().int().min(0).optional(), + }) + .strict(); + +const providerBreakerProfileSchema = z + .object({ + failureThreshold: z.number().int().min(1).optional(), + resetTimeoutMs: z.number().int().min(1000).optional(), + }) + .strict(); + +const waitForCooldownSettingsSchema = z + .object({ + enabled: z.boolean().optional(), + maxRetries: z.number().int().min(0).max(10).optional(), + maxRetryWaitSec: z.number().int().min(0).max(300).optional(), + }) + .strict(); + export const updateResilienceSchema = z .object({ - profiles: z + requestQueue: requestQueueSettingsSchema.optional(), + connectionCooldown: z .object({ - oauth: resilienceProfileSchema.optional(), - apikey: resilienceProfileSchema.optional(), + oauth: connectionCooldownProfileSchema.optional(), + apikey: connectionCooldownProfileSchema.optional(), }) .strict() .optional(), - defaults: resilienceDefaultsSchema.optional(), + providerBreaker: z + .object({ + oauth: providerBreakerProfileSchema.optional(), + apikey: providerBreakerProfileSchema.optional(), + }) + .strict() + .optional(), + waitForCooldown: waitForCooldownSettingsSchema.optional(), + profiles: z + .object({ + oauth: legacyResilienceProfileSchema.optional(), + apikey: legacyResilienceProfileSchema.optional(), + }) + .strict() + .optional(), + defaults: legacyResilienceDefaultsSchema.optional(), }) + .strict() .superRefine((value, ctx) => { - if (!value.profiles && !value.defaults) { + if ( + !value.requestQueue && + !value.connectionCooldown && + !value.providerBreaker && + !value.waitForCooldown && + !value.profiles && + !value.defaults + ) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Must provide profiles or defaults", + message: "Must provide resilience settings to update", path: [], }); } diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 5bc6b9629b..b66d1ea534 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -42,11 +42,6 @@ import { // Pipeline integration — wired modules import { getCircuitBreaker } from "../../shared/utils/circuitBreaker"; -import { - isModelAvailable, - markModelAsProblematic, - clearModelUnavailability, -} from "../../domain/modelAvailability"; import { markAccountExhaustedFrom429 } from "../../domain/quotaCache"; import { RequestTelemetry, recordTelemetry } from "../../shared/utils/requestTelemetry"; import { generateRequestId } from "../../shared/utils/requestId"; @@ -107,6 +102,8 @@ function intersectAllowedConnectionIds(primary: unknown, secondary: unknown): st return first || second || null; } +const PROVIDER_BREAKER_FAILURE_STATUSES = new Set([408, 500, 502, 503, 504]); + /** * Handle chat completion request * Supports: OpenAI, Claude, Gemini, OpenAI Responses API formats @@ -324,14 +321,6 @@ export async function handleChat(request: any, clientRawRequest: any = null) { return false; } - // Fixed-account combo steps must bypass the provider/model cooldown gate here. - // A previous account failure can quarantine the model globally, but the next - // step may intentionally pin a different connection for the same model. - if (!hasForcedConnection && !isModelAvailable(provider, resolvedModel)) { - log.debug("AVAILABILITY", `${provider}/${modelInfo.model} in cooldown, skipping`); - return false; - } - const creds = await getProviderCredentialsWithQuotaPreflight( provider, null, @@ -524,7 +513,7 @@ async function handleSingleModelChat( ? "fixed combo step connection" : undefined; - // 2. Pipeline gates (availability + circuit breaker) + // 2. Pipeline gates (availability + provider circuit breaker) const providerProfile = await getRuntimeProviderProfile(provider); const gate = await checkPipelineGates(provider, model, { ignoreCircuitBreaker: forceLiveComboTest || hasForcedConnection, @@ -535,8 +524,8 @@ async function handleSingleModelChat( if (gate) return gate; const breaker = getCircuitBreaker(provider, { - failureThreshold: providerProfile.circuitBreakerThreshold, - resetTimeout: providerProfile.circuitBreakerReset, + failureThreshold: providerProfile.failureThreshold, + resetTimeout: providerProfile.resetTimeoutMs, onStateChange: (name: string, from: string, to: string) => log.info("CIRCUIT", `${name}: ${from} → ${to}`), }); @@ -549,9 +538,10 @@ async function handleSingleModelChat( const retrySettings = disableCooldownAwareRetry ? { ...baseRetrySettings, - requestRetry: 0, - maxRetryIntervalSec: 0, - maxRetryIntervalMs: 0, + enabled: false, + maxRetries: 0, + maxRetryWaitSec: 0, + maxRetryWaitMs: 0, } : baseRetrySettings; const requestSignal = request?.signal ?? null; @@ -597,26 +587,6 @@ async function handleSingleModelChat( ); if (!credentials || "allRateLimited" in credentials) { - if ([408, 429, 500, 502, 503, 504].includes(Number(lastStatus))) { - const quarantine = markModelAsProblematic(provider, model, { - status: Number(lastStatus), - baseCooldownMs: lastCooldownMs, - reason: `HTTP ${lastStatus}`, - profile: providerProfile, - }); - if (quarantine.quarantined) { - log.info( - "AVAILABILITY", - `${provider}/${model} marked unavailable — all accounts exhausted (HTTP ${lastStatus}, cooldown ${Math.ceil(quarantine.cooldownMs / 1000)}s, failureCount ${quarantine.failureCount}/${quarantine.threshold})` - ); - } else { - log.info( - "AVAILABILITY", - `${provider}/${model} recorded exhaustion failure ${quarantine.failureCount}/${quarantine.threshold} (HTTP ${lastStatus}, cooldown basis ${Math.ceil(quarantine.cooldownMs / 1000)}s)` - ); - } - } - if (credentials?.allRateLimited) { const retryDecision = getCooldownAwareRetryDecision({ retryAfter: credentials.retryAfter, @@ -628,7 +598,7 @@ async function handleSingleModelChat( const waitSec = Math.max(Math.ceil(retryDecision.waitMs / 1000), 0); log.info( "COOLDOWN_RETRY", - `${provider}/${model} all accounts cooling down (${retryDecision.retryAfterHuman || `retry in ${waitSec}s`}) — waiting ${waitSec}s before retry ${requestRetryAttempt + 1}/${retrySettings.requestRetry}` + `${provider}/${model} all connections cooling down (${retryDecision.retryAfterHuman || `retry in ${waitSec}s`}) — waiting ${waitSec}s before retry ${requestRetryAttempt + 1}/${retrySettings.maxRetries}` ); const completed = await waitForCooldownAwareRetry(retryDecision.waitMs, requestSignal); @@ -643,12 +613,21 @@ async function handleSingleModelChat( requestRetryAttempt += 1; log.info( "COOLDOWN_RETRY", - `${provider}/${model} cooldown elapsed — restarting request attempt ${requestRetryAttempt}/${retrySettings.requestRetry}` + `${provider}/${model} cooldown elapsed — restarting request attempt ${requestRetryAttempt}/${retrySettings.maxRetries}` ); continue requestAttemptLoop; } } + const breakerFailureStatus = Number(lastStatus ?? credentials?.lastErrorCode); + if ( + !forceLiveComboTest && + credentials?.allRateLimited && + PROVIDER_BREAKER_FAILURE_STATUSES.has(breakerFailureStatus) + ) { + breaker._onFailure(); + } + return handleNoCredentials( credentials, excludedConnectionIds.size > 0 ? Array.from(excludedConnectionIds)[0] : null, @@ -717,11 +696,9 @@ async function handleSingleModelChat( const proxyInfo = await safeResolveProxy(credentials.connectionId); const proxyStartTime = Date.now(); - // 4. Execute chat via core (with circuit breaker + optional TLS) + // 4. Execute chat via core after breaker gate checks (with optional TLS tracking) if (telemetry) telemetry.startPhase("connect"); const { result, tlsFingerprintUsed } = await executeChatWithBreaker({ - bypassCircuitBreaker: forceLiveComboTest, - breaker, body: requestBody, provider, model, @@ -765,7 +742,9 @@ async function handleSingleModelChat( if (result.success) { clearModelLock(provider, credentials.connectionId, model); - clearModelUnavailability(provider, model); + if (!forceLiveComboTest) { + breaker._onSuccess(); + } if (injectedHandoff && runtimeOptions.sessionId && comboName) { deleteHandoff(runtimeOptions.sessionId, comboName); } @@ -841,7 +820,11 @@ async function handleSingleModelChat( // 6. Mark account as quota-exhausted on 429 response // For providers that route quota/cooldown at model scope, a 429 on one model // does not mean the whole connection is exhausted. - if (result.status === 429 && shouldMarkAccountExhaustedFrom429(provider, model)) { + const passthroughModels = credentials.providerSpecificData?.passthroughModels; + if ( + result.status === 429 && + shouldMarkAccountExhaustedFrom429(provider, model, passthroughModels) + ) { markAccountExhaustedFrom429(credentials.connectionId, provider); } @@ -869,6 +852,10 @@ async function handleSingleModelChat( continue; } + if (!forceLiveComboTest && PROVIDER_BREAKER_FAILURE_STATUSES.has(Number(result.status))) { + breaker._onFailure(); + } + return result.response; } } diff --git a/src/sse/handlers/chatHelpers.ts b/src/sse/handlers/chatHelpers.ts index f091220573..8099889ed3 100644 --- a/src/sse/handlers/chatHelpers.ts +++ b/src/sse/handlers/chatHelpers.ts @@ -14,6 +14,7 @@ import { handleChatCore } from "@omniroute/open-sse/handlers/chatCore.ts"; import { errorResponse, modelCooldownResponse, + providerCircuitOpenResponse, unavailableResponse, } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; @@ -23,8 +24,7 @@ import { isTlsFingerprintActive, } from "@omniroute/open-sse/utils/proxyFetch.ts"; import { resolveProxyForConnection } from "@/lib/localDb"; -import { getCircuitBreaker, CircuitBreakerOpenError } from "../../shared/utils/circuitBreaker"; -import { getModelCooldownInfo, isModelAvailable } from "../../domain/modelAvailability"; +import { getCircuitBreaker } from "../../shared/utils/circuitBreaker"; import { logProxyEvent } from "../../lib/proxyLogger"; import { logTranslationEvent } from "../../lib/translatorEvents"; import { getRuntimeProviderProfile } from "@omniroute/open-sse/services/accountFallback.ts"; @@ -76,30 +76,16 @@ export async function checkPipelineGates( providerProfile?: { circuitBreakerThreshold?: number; circuitBreakerReset?: number; + failureThreshold?: number; + resetTimeoutMs?: number; } | null; } = {} ) { const bypassReason = options.bypassReason || "pipeline override"; - const modelAvailable = isModelAvailable(provider, model); - if (!modelAvailable && options.ignoreModelCooldown) { - log.info("AVAILABILITY", `${provider}/${model} cooldown bypassed (${bypassReason})`); - } else if (!modelAvailable) { - const cooldownInfo = getModelCooldownInfo(provider, model); - const retryAfterSec = cooldownInfo - ? Math.max(Math.ceil(cooldownInfo.remainingMs / 1000), 1) - : 1; - log.warn("AVAILABILITY", `${provider}/${model} is in cooldown, rejecting request`); - return unavailableResponse( - HTTP_STATUS.SERVICE_UNAVAILABLE, - `Model ${provider}/${model} is temporarily unavailable (cooldown)`, - retryAfterSec - ); - } - const providerProfile = options.providerProfile ?? (await getRuntimeProviderProfile(provider)); const breaker = getCircuitBreaker(provider, { - failureThreshold: providerProfile.circuitBreakerThreshold, - resetTimeout: providerProfile.circuitBreakerReset, + failureThreshold: providerProfile.failureThreshold ?? providerProfile.circuitBreakerThreshold, + resetTimeout: providerProfile.resetTimeoutMs ?? providerProfile.circuitBreakerReset, onStateChange: (name: string, from: string, to: string) => log.info("CIRCUIT", `${name}: ${from} → ${to}`), }); @@ -109,19 +95,13 @@ export async function checkPipelineGates( const retryAfterMs = breaker.getRetryAfterMs(); const retryAfterSec = Math.max(Math.ceil(retryAfterMs / 1000), 1); log.warn("CIRCUIT", `Circuit breaker OPEN for ${provider}, rejecting request`); - return unavailableResponse( - HTTP_STATUS.SERVICE_UNAVAILABLE, - `Provider ${provider} circuit breaker is open`, - retryAfterSec - ); + return providerCircuitOpenResponse(provider, retryAfterSec); } return null; } export async function executeChatWithBreaker({ - bypassCircuitBreaker, - breaker, body, provider, model, @@ -174,40 +154,14 @@ export async function executeChatWithBreaker({ }) ); - if (bypassCircuitBreaker) { - if (!proxyInfo?.proxy && isTlsFingerprintActive()) { - const tracked = await runWithTlsTracking(chatFn); - return { result: tracked.result, tlsFingerprintUsed: tracked.tlsFingerprintUsed }; - } - - const result = await chatFn(); - return { result, tlsFingerprintUsed: false }; - } - if (!proxyInfo?.proxy && isTlsFingerprintActive()) { - const tracked = await breaker.execute(async () => runWithTlsTracking(chatFn)); + const tracked = await runWithTlsTracking(chatFn); return { result: tracked.result, tlsFingerprintUsed: tracked.tlsFingerprintUsed }; } - const result = await breaker.execute(chatFn); + const result = await chatFn(); return { result, tlsFingerprintUsed: false }; } catch (cbErr: any) { - if (cbErr instanceof CircuitBreakerOpenError) { - log.warn("CIRCUIT", `${provider} circuit open during retry: ${cbErr.message}`); - return { - result: { - success: false, - response: unavailableResponse( - HTTP_STATUS.SERVICE_UNAVAILABLE, - `Provider ${provider} circuit breaker is open`, - Math.ceil(cbErr.retryAfterMs / 1000) - ), - status: HTTP_STATUS.SERVICE_UNAVAILABLE, - }, - tlsFingerprintUsed: false, - }; - } - if (cbErr?.code === "PROXY_UNREACHABLE" || /proxy unreachable/i.test(cbErr?.message || "")) { const detail = cbErr?.message || "Proxy unreachable"; log.warn("PROXY", detail); diff --git a/src/sse/services/auth.ts b/src/sse/services/auth.ts index 63c611db9d..59e64366ad 100644 --- a/src/sse/services/auth.ts +++ b/src/sse/services/auth.ts @@ -23,6 +23,10 @@ import { import { isLocalProvider } from "@omniroute/open-sse/config/providerRegistry.ts"; import { COOLDOWN_MS } from "@omniroute/open-sse/config/constants.ts"; import { preflightQuota } from "@omniroute/open-sse/services/quotaPreflight.ts"; +import { + classifyProviderError, + PROVIDER_ERROR_TYPES, +} from "@omniroute/open-sse/services/errorClassifier.ts"; import { getCodexModelScope } from "@omniroute/open-sse/executors/codex.ts"; import { getProviderAlias, resolveProviderId } from "@/shared/constants/providers"; import { isModelExcludedByConnection } from "@/domain/connectionModelRules"; @@ -251,6 +255,31 @@ function isTerminalConnectionStatus(connection: ProviderConnectionView): boolean return status === "credits_exhausted" || status === "banned" || status === "expired"; } +function resolveTerminalConnectionStatus( + status: number, + result: { permanent?: boolean; creditsExhausted?: boolean }, + providerErrorType: string | null = null +): string | null { + if (result.creditsExhausted || status === 402) return "credits_exhausted"; + if ( + providerErrorType === PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR || + providerErrorType === PROVIDER_ERROR_TYPES.OAUTH_INVALID_TOKEN + ) { + return null; + } + if (result.permanent || providerErrorType === PROVIDER_ERROR_TYPES.FORBIDDEN || status === 403) { + return "banned"; + } + if ( + providerErrorType === PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED || + providerErrorType === PROVIDER_ERROR_TYPES.UNAUTHORIZED || + status === 401 + ) { + return "expired"; + } + return null; +} + export function resolveQuotaLimitPolicy( provider: string, providerSpecificData: JsonRecord @@ -1188,22 +1217,39 @@ export async function markAccountUnavailable( const effectiveProviderProfile = providerProfile || (provider ? await getRuntimeProviderProfile(provider) : null); + const fallbackResult = checkFallbackError( + status, + errorText, + backoffLevel, + model, + provider, + null, + effectiveProviderProfile + ); - const isPerModelQuotaProvider = hasPerModelQuota(provider, model); + // Read passthroughModels from connection config (user-configured per-model quota) + const connProviderSpecificData = (conn?.providerSpecificData as Record) || {}; + const connectionPassthroughModels = connProviderSpecificData.passthroughModels as + | boolean + | undefined; + + const isPerModelQuotaProvider = hasPerModelQuota(provider, model, connectionPassthroughModels); if (isPerModelQuotaProvider && provider && model && (status === 404 || status === 429)) { const reason = status === 404 ? "not_found" : "rate_limited"; - const fallbackCooldown = - status === 404 - ? (effectiveProviderProfile?.transientCooldown ?? COOLDOWN_MS.notFoundLocal) - : 0; const lockout = recordModelLockoutFailure( provider, connectionId, model, reason, status, - fallbackCooldown, - effectiveProviderProfile + status === 404 + ? (effectiveProviderProfile?.baseCooldownMs ?? COOLDOWN_MS.notFoundLocal) + : (fallbackResult.baseCooldownMs ?? effectiveProviderProfile?.baseCooldownMs ?? 0), + effectiveProviderProfile, + { + exactCooldownMs: + fallbackResult.usedUpstreamRetryHint === true ? fallbackResult.cooldownMs : null, + } ); // Update last error for observability (without changing terminal status) updateProviderConnection(connectionId, { @@ -1218,18 +1264,12 @@ export async function markAccountUnavailable( ); return { shouldFallback: true, cooldownMs: lockout.cooldownMs }; } - - const result = checkFallbackError( - status, - errorText, - backoffLevel, - model, - provider, - null, - effectiveProviderProfile - ); - const { shouldFallback, cooldownMs, newBackoffLevel, reason } = result; + const result = fallbackResult; + const { shouldFallback, cooldownMs: rawCooldownMs, newBackoffLevel, reason } = result; if (!shouldFallback) return { shouldFallback: false, cooldownMs: 0 }; + const providerErrorType = classifyProviderError(status, errorText); + const terminalStatus = resolveTerminalConnectionStatus(status, result, providerErrorType); + const cooldownMs = terminalStatus ? 0 : rawCooldownMs; // ── 404 model-only lockout: connection stays active ── // For local providers (detected by URL), a 404 means the specific model @@ -1262,7 +1302,6 @@ export async function markAccountUnavailable( return { shouldFallback: true, cooldownMs: lockout.cooldownMs }; } - const rateLimitedUntil = getUnavailableUntil(cooldownMs); const errorMsg = typeof errorText === "string" ? errorText.slice(0, 100) : "Provider error"; // T09: Codex per-scope lockout (do not block the whole account globally). @@ -1270,7 +1309,7 @@ export async function markAccountUnavailable( const scope = getCodexModelScope(model); const existingScopeMap = asRecord(conn.providerSpecificData.codexScopeRateLimitedUntil); const persistedScopeUntil = getCodexScopeRateLimitedUntil(conn.providerSpecificData, model); - const scopeRateLimitedUntil = persistedScopeUntil || rateLimitedUntil; + const scopeRateLimitedUntil = persistedScopeUntil || getUnavailableUntil(cooldownMs); const scopeCooldownMs = Math.max(new Date(scopeRateLimitedUntil).getTime() - Date.now(), 0); await updateProviderConnection(connectionId, { @@ -1299,14 +1338,27 @@ export async function markAccountUnavailable( return { shouldFallback: true, cooldownMs: scopeCooldownMs }; } - await updateProviderConnection(connectionId, { - rateLimitedUntil, - testStatus: "unavailable", + const baseUpdate = { lastError: errorMsg, + lastErrorType: providerErrorType, errorCode: status, lastErrorAt: new Date().toISOString(), backoffLevel: newBackoffLevel ?? backoffLevel, - }); + }; + + if (cooldownMs > 0) { + await updateProviderConnection(connectionId, { + ...baseUpdate, + rateLimitedUntil: getUnavailableUntil(cooldownMs), + testStatus: "unavailable", + }); + } else { + await updateProviderConnection(connectionId, { + ...baseUpdate, + rateLimitedUntil: null, + ...(terminalStatus ? { testStatus: terminalStatus } : {}), + }); + } // T-AUTODISABLE: If auto-disable setting is enabled and error is permanent/terminal, // mark account as inactive so it is never retried again. @@ -1330,11 +1382,6 @@ export async function markAccountUnavailable( } } - // Per-model lockout: lock the specific model if known - if (provider && model && cooldownMs > 0) { - lockModel(provider, connectionId, model, reason || "unknown", cooldownMs); - } - if (provider && status && errorMsg) { console.error(`❌ ${provider} [${status}]: ${errorMsg}`); } diff --git a/src/sse/services/cooldownAwareRetry.ts b/src/sse/services/cooldownAwareRetry.ts index 99f1ef5f4a..14a95c605d 100644 --- a/src/sse/services/cooldownAwareRetry.ts +++ b/src/sse/services/cooldownAwareRetry.ts @@ -1,14 +1,14 @@ import { formatRetryAfter } from "@omniroute/open-sse/services/accountFallback.ts"; +import { resolveResilienceSettings } from "@/lib/resilience/settings"; -const DEFAULT_REQUEST_RETRY = 3; -const DEFAULT_MAX_RETRY_INTERVAL_SEC = 30; const MAX_REQUEST_RETRY = 10; const MAX_RETRY_INTERVAL_SEC = 300; export interface CooldownAwareRetrySettings { - requestRetry: number; - maxRetryIntervalSec: number; - maxRetryIntervalMs: number; + enabled: boolean; + maxRetries: number; + maxRetryWaitSec: number; + maxRetryWaitMs: number; } function normalizeInteger( @@ -35,20 +35,26 @@ function normalizeInteger( export function resolveCooldownAwareRetrySettings( settings: Record | null | undefined ): CooldownAwareRetrySettings { - const requestRetry = normalizeInteger(settings?.requestRetry, DEFAULT_REQUEST_RETRY, { + const waitForCooldown = resolveResilienceSettings(settings).waitForCooldown; + const maxRetries = normalizeInteger(waitForCooldown.maxRetries, waitForCooldown.maxRetries, { min: 0, max: MAX_REQUEST_RETRY, }); - const maxRetryIntervalSec = normalizeInteger( - settings?.maxRetryIntervalSec, - DEFAULT_MAX_RETRY_INTERVAL_SEC, - { min: 0, max: MAX_RETRY_INTERVAL_SEC } + const maxRetryWaitSec = normalizeInteger( + waitForCooldown.maxRetryWaitSec, + waitForCooldown.maxRetryWaitSec, + { + min: 0, + max: MAX_RETRY_INTERVAL_SEC, + } ); + const enabled = Boolean(waitForCooldown.enabled) && maxRetries > 0 && maxRetryWaitSec > 0; return { - requestRetry, - maxRetryIntervalSec, - maxRetryIntervalMs: maxRetryIntervalSec * 1000, + enabled, + maxRetries, + maxRetryWaitSec, + maxRetryWaitMs: maxRetryWaitSec * 1000, }; } @@ -90,9 +96,10 @@ export function getCooldownAwareRetryDecision({ } { const closest = computeClosestRetryAfter(retryAfter); if ( - settings.requestRetry <= 0 || - settings.maxRetryIntervalMs <= 0 || - attempt >= settings.requestRetry || + !settings.enabled || + settings.maxRetries <= 0 || + settings.maxRetryWaitMs <= 0 || + attempt >= settings.maxRetries || closest.waitMs === null ) { return { @@ -103,7 +110,7 @@ export function getCooldownAwareRetryDecision({ }; } - if (closest.waitMs > settings.maxRetryIntervalMs) { + if (closest.waitMs > settings.maxRetryWaitMs) { return { shouldRetry: false, retryAfter: closest.retryAfter, diff --git a/src/types/settings.ts b/src/types/settings.ts index 9f4f32b38e..273b5bf941 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -1,4 +1,5 @@ import type { HideableSidebarItemId } from "@/shared/constants/sidebarVisibility"; +import type { ResilienceSettings } from "@/lib/resilience/settings"; /** * Application settings stored in SQLite key-value pairs. @@ -20,15 +21,13 @@ export interface Settings { jwtSecret?: string; hideHealthCheckLogs?: boolean; hiddenSidebarItems?: HideableSidebarItemId[]; + resilienceSettings?: ResilienceSettings; } export interface ComboDefaults { strategy: "priority" | "weighted" | "round-robin" | "context-relay"; maxRetries: number; retryDelayMs: number; - timeoutMs: number; - healthCheckEnabled: boolean; - healthCheckTimeoutMs: number; maxComboDepth: number; trackMetrics: boolean; concurrencyPerModel?: number; diff --git a/tests/e2e/resilience-plan-alignment.spec.ts b/tests/e2e/resilience-plan-alignment.spec.ts new file mode 100644 index 0000000000..817ebcf5bc --- /dev/null +++ b/tests/e2e/resilience-plan-alignment.spec.ts @@ -0,0 +1,374 @@ +import { expect, test, type Page } from "@playwright/test"; +import { gotoDashboardRoute } from "./helpers/dashboardAuth"; + +const resilienceSettings = { + requestQueue: { + autoEnableApiKeyProviders: true, + requestsPerMinute: 100, + minTimeBetweenRequestsMs: 200, + concurrentRequests: 10, + maxWaitMs: 120000, + }, + connectionCooldown: { + oauth: { + baseCooldownMs: 60000, + useUpstreamRetryHints: false, + maxBackoffSteps: 8, + }, + apikey: { + baseCooldownMs: 3000, + useUpstreamRetryHints: true, + maxBackoffSteps: 5, + }, + }, + providerBreaker: { + oauth: { + failureThreshold: 3, + resetTimeoutMs: 60000, + }, + apikey: { + failureThreshold: 5, + resetTimeoutMs: 30000, + }, + }, + waitForCooldown: { + enabled: true, + maxRetries: 3, + maxRetryWaitSec: 30, + }, +}; + +async function mockResilienceSettings(page: Page) { + await page.route("**/api/resilience", async (route) => { + const method = route.request().method(); + if (method === "GET") { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(resilienceSettings), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + ok: true, + ...resilienceSettings, + }), + }); + }); +} + +async function mockHealthPageApis(page: Page) { + const now = new Date().toISOString(); + + await page.route("**/api/monitoring/health", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + status: "error", + system: { + uptime: 3723, + version: "3.7.0", + nodeVersion: "22.12.0", + memoryUsage: { + rss: 64 * 1024 * 1024, + heapUsed: 24 * 1024 * 1024, + heapTotal: 48 * 1024 * 1024, + }, + }, + providerHealth: { + openai: { + state: "OPEN", + failures: 3, + retryAfterMs: 15000, + lastFailure: now, + }, + gemini: { + state: "HALF_OPEN", + failures: 1, + retryAfterMs: 5000, + lastFailure: now, + }, + groq: { + state: "CLOSED", + failures: 0, + retryAfterMs: 0, + lastFailure: null, + }, + }, + providerSummary: { + configuredCount: 3, + activeCount: 2, + monitoredCount: 3, + }, + rateLimitStatus: {}, + lockouts: {}, + sessions: { + activeCount: 0, + stickyBoundCount: 0, + byApiKey: {}, + top: [], + }, + quotaMonitor: { + active: 0, + alerting: 0, + exhausted: 0, + errors: 0, + byProvider: {}, + monitors: [], + }, + }), + }); + }); + + await page.route("**/api/telemetry/summary", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ p50: 120, p95: 240, p99: 450, totalRequests: 18 }), + }); + }); + + await page.route("**/api/cache/stats", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ size: 3, maxSize: 100, hitRate: 50, hits: 2, misses: 2 }), + }); + }); + + await page.route("**/api/rate-limits", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + cacheStats: { + defaultCount: 0, + tool: { entries: 0, patterns: 0 }, + family: { entries: 0, patterns: 0 }, + session: { entries: 0, patterns: 0 }, + }, + }), + }); + }); + + await page.route("**/api/health/degradation", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + summary: { full: 0, reduced: 0, minimal: 0, default: 0 }, + features: [], + }), + }); + }); + + await page.route("**/api/v1/db/health", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + isHealthy: true, + issues: [], + repairedCount: 0, + backupCreated: false, + }), + }); + }); +} + +async function mockProvidersPageApis(page: Page) { + await page.route("**/api/providers", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + connections: [ + { + id: "conn-openai-main", + provider: "openai", + authType: "apikey", + name: "OpenAI Main", + testStatus: "active", + }, + { + id: "conn-gemini-main", + provider: "gemini", + authType: "apikey", + name: "Gemini Main", + testStatus: "active", + }, + ], + }), + }); + }); + + await page.route("**/api/provider-nodes", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ nodes: [], ccCompatibleProviderEnabled: false }), + }); + }); + + await page.route("**/api/providers/expiration", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({}), + }); + }); + + await page.route("**/api/system/env/repair", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ available: false, missingCount: 0 }), + }); + }); +} + +async function mockIntelligentCombosPageApis(page: Page) { + await page.route("**/api/combos", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + combos: [ + { + id: "combo-auto", + name: "combo-auto", + models: ["openai/gpt-4o-mini", "gemini/gemini-2.5-pro"], + strategy: "auto", + config: { + candidatePool: ["openai", "gemini", "anthropic"], + modePack: "ship-fast", + explorationRate: 0.15, + }, + isActive: true, + }, + ], + }), + }); + }); + + await page.route("**/api/combos/metrics", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ metrics: {} }), + }); + }); + + await page.route("**/api/providers", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + connections: [ + { id: "conn-openai", provider: "openai", name: "OpenAI", testStatus: "active" }, + { id: "conn-gemini", provider: "gemini", name: "Gemini", testStatus: "active" }, + { + id: "conn-anthropic", + provider: "anthropic", + name: "Anthropic", + testStatus: "active", + }, + ], + }), + }); + }); + + await page.route("**/api/provider-nodes", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ nodes: [] }), + }); + }); + + await page.route("**/api/settings/proxy", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ combos: {} }), + }); + }); +} + +test.describe("Resilience Plan Alignment", () => { + test("resilience settings page only shows the plan-aligned cooldown fields", async ({ page }) => { + await mockResilienceSettings(page); + + await gotoDashboardRoute(page, "/dashboard/settings?tab=resilience"); + await expect(page.getByText("Connection Cooldown")).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("Base cooldown", { exact: true }).first()).toBeVisible(); + await expect(page.getByText("Use upstream retry hints", { exact: true }).first()).toBeVisible(); + await expect(page.getByText("Max backoff steps", { exact: true }).first()).toBeVisible(); + await expect(page.getByText(/Rate-limit fallback/i)).toHaveCount(0); + }); + + test("health page renders provider breaker runtime state for multiple providers", async ({ + page, + }) => { + await mockHealthPageApis(page); + + await gotoDashboardRoute(page, "/dashboard/health"); + await expect(page.getByText("Provider Health")).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("OpenAI")).toBeVisible(); + await expect(page.getByText("Gemini")).toBeVisible(); + await expect(page.getByText("Groq")).toBeVisible(); + await expect(page.getByText("Recovering", { exact: true }).first()).toBeVisible(); + await expect(page.getByText("Down", { exact: true }).first()).toBeVisible(); + }); + + test("providers page no longer requests legacy model availability data", async ({ page }) => { + let availabilityRequests = 0; + + await mockProvidersPageApis(page); + await page.route("**/api/models/availability", async (route) => { + availabilityRequests += 1; + await route.fulfill({ + status: 410, + contentType: "application/json", + body: JSON.stringify({ error: "removed" }), + }); + }); + + await gotoDashboardRoute(page, "/dashboard/providers"); + await page.waitForLoadState("networkidle"); + + expect(availabilityRequests).toBe(0); + await expect(page.getByText("OpenAI").first()).toBeVisible(); + await expect(page.getByText(/Model Availability/i)).toHaveCount(0); + }); + + test("intelligent combo panel stays config-only and does not fetch breaker runtime state", async ({ + page, + }) => { + let monitoringHealthRequests = 0; + + await mockIntelligentCombosPageApis(page); + await page.route("**/api/monitoring/health", async (route) => { + monitoringHealthRequests += 1; + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ providerHealth: {} }), + }); + }); + + await gotoDashboardRoute(page, "/dashboard/combos?filter=intelligent"); + await page.waitForLoadState("networkidle"); + + expect(monitoringHealthRequests).toBe(0); + await expect(page.getByText("Intelligent Routing Dashboard")).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("Routing Inputs", { exact: true })).toBeVisible(); + await expect(page.getByText(/Excluded Providers/i)).toHaveCount(0); + await expect(page.getByText(/Incident Mode/i)).toHaveCount(0); + }); +}); diff --git a/tests/integration/_chatPipelineHarness.ts b/tests/integration/_chatPipelineHarness.ts index d5a7cdbad1..57b2d745ed 100644 --- a/tests/integration/_chatPipelineHarness.ts +++ b/tests/integration/_chatPipelineHarness.ts @@ -33,8 +33,6 @@ export async function createChatPipelineHarness(prefix) { const { initTranslators } = await import("../../open-sse/translator/index.ts"); const { clearInflight } = await import("../../open-sse/services/requestDedup.ts"); const { BaseExecutor } = await import("../../open-sse/executors/base.ts"); - const { resetAllAvailability, setModelUnavailable } = - await import("../../src/domain/modelAvailability.ts"); const { resetAllCircuitBreakers } = await import("../../src/shared/utils/circuitBreaker.ts"); const originalFetch = globalThis.fetch; @@ -209,7 +207,6 @@ export async function createChatPipelineHarness(prefix) { clearInflight(); idempotencyLayerModule.clearIdempotency(); semanticCacheModule.clearCache(); - resetAllAvailability(); resetAllCircuitBreakers(); apiKeysDb.resetApiKeyState(); readCacheDb.invalidateDbCache(); @@ -229,7 +226,6 @@ export async function createChatPipelineHarness(prefix) { idempotencyLayerModule.clearIdempotency(); semanticCacheModule.clearCache(); clearSkillState(); - resetAllAvailability(); resetAllCircuitBreakers(); core.resetDbInstance(); fs.rmSync(testDataDir, { recursive: true, force: true }); @@ -292,7 +288,6 @@ export async function createChatPipelineHarness(prefix) { semanticCacheModule, seedApiKey, seedConnection, - setModelUnavailable, settingsDb, skillByIdRouteModule, skillExecutor, diff --git a/tests/integration/chat-pipeline.test.ts b/tests/integration/chat-pipeline.test.ts index 3723cc9411..ce6f4641b3 100644 --- a/tests/integration/chat-pipeline.test.ts +++ b/tests/integration/chat-pipeline.test.ts @@ -23,9 +23,9 @@ const { handleChat } = await import("../../src/sse/handlers/chat.ts"); const { initTranslators } = await import("../../open-sse/translator/index.ts"); const { clearInflight } = await import("../../open-sse/services/requestDedup.ts"); const { BaseExecutor } = await import("../../open-sse/executors/base.ts"); -const { resetAllAvailability, setModelUnavailable } = - await import("../../src/domain/modelAvailability.ts"); -const { resetAllCircuitBreakers } = await import("../../src/shared/utils/circuitBreaker.ts"); +const { getCircuitBreaker, resetAllCircuitBreakers } = + await import("../../src/shared/utils/circuitBreaker.ts"); +const { clearProviderFailure } = await import("../../open-sse/services/accountFallback.ts"); const originalFetch = globalThis.fetch; const originalRetryDelayMs = BaseExecutor.RETRY_CONFIG.delayMs; @@ -294,7 +294,6 @@ async function resetStorage() { globalThis.fetch = originalFetch; process.env.REQUIRE_API_KEY = "false"; clearInflight(); - resetAllAvailability(); resetAllCircuitBreakers(); apiKeysDb.resetApiKeyState(); readCacheDb.invalidateDbCache(); @@ -441,7 +440,6 @@ test.after(async () => { BaseExecutor.RETRY_CONFIG.delayMs = originalRetryDelayMs; globalThis.fetch = originalFetch; clearInflight(); - resetAllAvailability(); resetAllCircuitBreakers(); core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); @@ -908,26 +906,6 @@ test("chat pipeline returns current no-credentials contract when no provider con assert.match(json.error.message, /No credentials for provider: openai/); }); -test("chat pipeline returns 503 when the requested model is temporarily unavailable", async () => { - await seedConnection("openai", { apiKey: "sk-openai-unavailable" }); - setModelUnavailable("openai", "gpt-4o-mini", 60000, "test cooldown"); - - const response = await handleChat( - buildRequest({ - body: { - model: "openai/gpt-4o-mini", - stream: false, - messages: [{ role: "user", content: "Provider unavailable" }], - }, - }) - ); - - const json = await response.json(); - assert.equal(response.status, 503); - assert.ok(Number(response.headers.get("Retry-After")) >= 1); - assert.match(json.error.message, /temporarily unavailable/i); -}); - test("chat pipeline surfaces upstream 500 responses as structured errors", async () => { await seedConnection("openai", { apiKey: "sk-openai-500" }); @@ -992,6 +970,46 @@ test("chat pipeline returns 429 with Retry-After when the upstream rate-limits t assert.match(json.error.message, /\[openai\/gpt-4o-mini\]/); }); +test("chat pipeline keeps provider breaker closed for repeated connection-scoped 429s", async () => { + await seedConnection("openai", { apiKey: "sk-openai-429-breaker" }); + await settingsDb.updateSettings({ + requestRetry: 0, + maxRetryIntervalSec: 0, + }); + + globalThis.fetch = async () => + new Response( + JSON.stringify({ + error: { + message: "Rate limit exceeded. Your quota will reset after 30s.", + }, + }), + { + status: 429, + headers: { "Content-Type": "application/json" }, + } + ); + + for (let i = 0; i < 3; i += 1) { + const response = await handleChat( + buildRequest({ + body: { + model: "openai/gpt-4o-mini", + stream: false, + messages: [{ role: "user", content: `Trigger 429 #${i + 1}` }], + }, + }) + ); + assert.equal(response.status, 429); + } + + const breaker = getCircuitBreaker("openai"); + const status = breaker.getStatus(); + + assert.equal(status.state, "CLOSED"); + assert.equal(status.failureCount, 0); +}); + test("chat pipeline maps upstream timeouts to 504 responses", async () => { await seedConnection("openai", { apiKey: "sk-openai-timeout" }); @@ -1017,6 +1035,8 @@ test("chat pipeline maps upstream timeouts to 504 responses", async () => { }); test("chat pipeline injects memory context before sending the upstream request", async () => { + // Reset provider failure state to avoid circuit breaker interference + clearProviderFailure("openai"); await seedConnection("openai", { apiKey: "sk-openai-memory" }); const apiKey = await seedApiKey(); await settingsDb.updateSettings({ @@ -1057,6 +1077,8 @@ test("chat pipeline injects memory context before sending the upstream request", }); test("chat pipeline injects skills into tools and intercepts tool calls with skill output", async () => { + // Reset provider failure state to avoid circuit breaker interference + clearProviderFailure("openai"); await seedConnection("openai", { apiKey: "sk-openai-skills" }); const apiKey = await seedApiKey(); await settingsDb.updateSettings({ skillsEnabled: true }); @@ -1118,6 +1140,8 @@ test("chat pipeline injects skills into tools and intercepts tool calls with ski }); test("chat pipeline falls back to the next account after a provider failure", async () => { + // Reset provider failure state to avoid circuit breaker interference + clearProviderFailure("openai"); await seedConnection("openai", { name: "openai-primary", apiKey: "sk-openai-primary-fallback", @@ -1162,6 +1186,9 @@ test("chat pipeline falls back to the next account after a provider failure", as }); test("chat pipeline falls back across combo models when the first provider fails", async () => { + // Reset provider failure state to avoid circuit breaker interference + clearProviderFailure("openai"); + clearProviderFailure("claude"); await seedConnection("openai", { apiKey: "sk-openai-combo-fail" }); await seedConnection("claude", { apiKey: "sk-claude-combo-fail" }); await combosDb.createCombo({ @@ -1206,6 +1233,8 @@ test("chat pipeline falls back across combo models when the first provider fails }); test("chat pipeline deduplicates concurrent identical non-stream requests", async () => { + // Reset provider failure state to avoid circuit breaker interference + clearProviderFailure("openai"); await seedConnection("openai", { apiKey: "sk-openai-dedup" }); let fetchCount = 0; diff --git a/tests/integration/chatcore-compression-integration.test.ts b/tests/integration/chatcore-compression-integration.test.ts index 13adcd92e6..2ab34d8fa0 100644 --- a/tests/integration/chatcore-compression-integration.test.ts +++ b/tests/integration/chatcore-compression-integration.test.ts @@ -15,14 +15,12 @@ const readCacheDb = await import("../../src/lib/db/readCache.ts"); const combosDb = await import("../../src/lib/db/combos.ts"); const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts"); const { estimateTokens, getTokenLimit } = await import("../../open-sse/services/contextManager.ts"); -const { resetAllAvailability } = await import("../../src/domain/modelAvailability.ts"); const { resetAllCircuitBreakers } = await import("../../src/shared/utils/circuitBreaker.ts"); const originalFetch = globalThis.fetch; async function resetStorage() { globalThis.fetch = originalFetch; - resetAllAvailability(); resetAllCircuitBreakers(); readCacheDb.invalidateDbCache(); await new Promise((resolve) => setTimeout(resolve, 20)); diff --git a/tests/integration/integration-wiring.test.ts b/tests/integration/integration-wiring.test.ts index a9e99a6809..4ecd2da607 100644 --- a/tests/integration/integration-wiring.test.ts +++ b/tests/integration/integration-wiring.test.ts @@ -84,8 +84,8 @@ describe("Pipeline Wiring — sse chat handler", () => { assert.match(src, /getCircuitBreaker|CircuitBreakerOpenError/); }); - it("should import model availability integration", () => { - assert.match(src, /isModelAvailable|setModelUnavailable/); + it("should use credential preflight instead of global model quarantine gates", () => { + assert.match(src, /getProviderCredentialsWithQuotaPreflight/); }); it("should import request telemetry integration", () => { @@ -129,7 +129,6 @@ describe("Pipeline Wiring — middleware proxy", () => { describe("API Routes — existence check", () => { const routes = [ "src/app/api/cache/stats/route.ts", - "src/app/api/models/availability/route.ts", "src/app/api/telemetry/summary/route.ts", "src/app/api/usage/budget/route.ts", "src/app/api/usage/quota/route.ts", @@ -152,10 +151,6 @@ describe("API Routes — export HTTP methods", () => { assertRouteMethods("src/app/api/cache/stats/route.ts", ["GET", "DELETE"]); }); - it("/api/models/availability should export GET and POST", () => { - assertRouteMethods("src/app/api/models/availability/route.ts", ["GET", "POST"]); - }); - it("/api/telemetry/summary should export GET", () => { assertRouteMethods("src/app/api/telemetry/summary/route.ts", ["GET"]); }); diff --git a/tests/integration/proxy-pipeline.test.ts b/tests/integration/proxy-pipeline.test.ts index 847c27cb4a..42540df355 100644 --- a/tests/integration/proxy-pipeline.test.ts +++ b/tests/integration/proxy-pipeline.test.ts @@ -88,25 +88,25 @@ describe("Chat Pipeline — combo fallback support", () => { assert.match(src, /handleSingleModel.*handleSingleModelChat/s); }); - it("should check model availability before attempting combo models", () => { - assert.match(src, /isModelAvailable/); + it("should preflight provider credentials before attempting combo models", () => { + assert.match(src, /getProviderCredentialsWithQuotaPreflight/); }); }); describe("Chat Pipeline — circuit breaker integration", () => { const helpersSrc = readSrc("sse/handlers/chatHelpers.ts"); - it("should import CircuitBreakerOpenError", () => { + it("should import providerCircuitOpenResponse", () => { assert.ok(helpersSrc, "chatHelpers.ts should exist"); - assert.match(helpersSrc, /CircuitBreakerOpenError/); + assert.match(helpersSrc, /providerCircuitOpenResponse/); }); - it("should handle CircuitBreakerOpenError with retry-after", () => { + it("should handle circuit-open responses with retry-after", () => { assert.match(helpersSrc, /retryAfterMs/); }); - it("should reject requests when circuit is open", () => { - assert.match(helpersSrc, /circuit breaker is open/i); + it("should reject requests when circuit is open via structured provider breaker response", () => { + assert.match(helpersSrc, /providerCircuitOpenResponse\(provider,\s*retryAfterSec\)/); }); }); diff --git a/tests/integration/resilience-http-e2e.test.ts b/tests/integration/resilience-http-e2e.test.ts new file mode 100644 index 0000000000..f6ea094ab2 --- /dev/null +++ b/tests/integration/resilience-http-e2e.test.ts @@ -0,0 +1,746 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import fsp from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import http from "node:http"; +import net from "node:net"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-resilience-http-e2e-")); +const DASHBOARD_PORT = await getFreePort(); +const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url)); + +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "resilience-http-e2e-secret-123456"; +process.env.REQUIRE_API_KEY = "false"; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const combosDb = await import("../../src/lib/db/combos.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); + +function getFreePort() { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (!address || typeof address === "string") { + server.close(); + reject(new Error("Failed to allocate a free port")); + return; + } + const { port } = address; + server.close((closeError) => { + if (closeError) reject(closeError); + else resolve(port); + }); + }); + }); +} + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function buildCompletion(content: string) { + return { + status: 200, + body: { + id: `chatcmpl_${Math.random().toString(16).slice(2, 8)}`, + object: "chat.completion", + choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }], + usage: { prompt_tokens: 4, completion_tokens: 2, total_tokens: 6 }, + }, + }; +} + +function buildError(status: number, message: string, headers: Record = {}) { + return { + status, + headers, + body: { error: { message } }, + }; +} + +type PlannedResponse = { + status: number; + body: Record; + headers?: Record; + delayMs?: number; +}; + +type TokenBehavior = { + defaultResponse: PlannedResponse; + queue: PlannedResponse[]; + hits: number; + startedAt: number[]; + bodies: Array>; +}; + +function createFakeOpenAiRelay() { + const behaviors = new Map(); + let server: http.Server | null = null; + let baseUrl = ""; + + const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => { + const chunks: Buffer[] = []; + req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); + req.on("end", async () => { + const authHeader = String(req.headers.authorization || ""); + const token = authHeader.replace(/^Bearer\s+/i, "").trim(); + const rawBody = Buffer.concat(chunks).toString("utf8"); + const parsedBody = rawBody ? JSON.parse(rawBody) : {}; + + if (req.method === "GET" && req.url?.startsWith("/v1/models")) { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ data: [{ id: "test-model", object: "model" }] })); + return; + } + + if (req.method !== "POST" || !req.url?.startsWith("/v1/chat/completions")) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: { message: `Unhandled path: ${req.method} ${req.url}` } })); + return; + } + + const behavior = behaviors.get(token); + if (!behavior) { + res.writeHead(401, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: { message: `Unknown token: ${token || "missing"}` } })); + return; + } + + behavior.hits += 1; + behavior.startedAt.push(Date.now()); + behavior.bodies.push(parsedBody as Record); + + const planned = behavior.queue.shift() || behavior.defaultResponse; + if (planned.delayMs && planned.delayMs > 0) { + await sleep(planned.delayMs); + } + + const headers = { "Content-Type": "application/json", ...(planned.headers || {}) }; + res.writeHead(planned.status, headers); + res.end(JSON.stringify(planned.body)); + }); + }; + + return { + async start() { + const port = await getFreePort(); + await new Promise((resolve, reject) => { + server = http.createServer((req, res) => { + void handleRequest(req, res); + }); + server.once("error", reject); + server.listen(port, "127.0.0.1", () => resolve()); + }); + baseUrl = `http://127.0.0.1:${port}/v1`; + return baseUrl; + }, + getBaseUrl() { + if (!baseUrl) throw new Error("Fake relay has not started yet"); + return baseUrl; + }, + configureToken( + token: string, + config: { defaultResponse: PlannedResponse; queue?: PlannedResponse[] } + ) { + behaviors.set(token, { + defaultResponse: config.defaultResponse, + queue: [...(config.queue || [])], + hits: 0, + startedAt: [], + bodies: [], + }); + }, + getState(token: string) { + const state = behaviors.get(token); + if (!state) throw new Error(`Unknown token state for ${token}`); + return state; + }, + resetState(token: string, queue?: PlannedResponse[]) { + const state = behaviors.get(token); + if (!state) throw new Error(`Unknown token state for ${token}`); + state.hits = 0; + state.startedAt = []; + state.bodies = []; + state.queue = [...(queue || [])]; + }, + async stop() { + if (!server) return; + await new Promise((resolve) => server?.close(() => resolve())); + server = null; + }, + }; +} + +function createServerProcess(dataDir: string, port: number) { + const stdoutLines: string[] = []; + const stderrLines: string[] = []; + const child = spawn(process.execPath, ["scripts/run-next.mjs", "dev"], { + cwd: REPO_ROOT, + env: { + ...process.env, + DATA_DIR: dataDir, + PORT: String(port), + DASHBOARD_PORT: String(port), + API_PORT: String(port), + HOST: "127.0.0.1", + REQUIRE_API_KEY: "false", + API_KEY_SECRET: process.env.API_KEY_SECRET || "resilience-http-e2e-secret-123456", + DISABLE_SQLITE_AUTO_BACKUP: "true", + INITIAL_PASSWORD: "", + NEXT_TELEMETRY_DISABLED: "1", + OMNIROUTE_DISABLE_BACKGROUND_SERVICES: "true", + OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK: "true", + OMNIROUTE_DISABLE_LOCAL_HEALTHCHECK: "true", + OMNIROUTE_HIDE_HEALTHCHECK_LOGS: "true", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + child.stdout.on("data", (chunk) => { + const lines = String(chunk).split(/\r?\n/).filter(Boolean); + stdoutLines.push(...lines); + if (stdoutLines.length > 200) stdoutLines.splice(0, stdoutLines.length - 200); + }); + child.stderr.on("data", (chunk) => { + const lines = String(chunk).split(/\r?\n/).filter(Boolean); + stderrLines.push(...lines); + if (stderrLines.length > 200) stderrLines.splice(0, stderrLines.length - 200); + }); + + return { + child, + stdoutLines, + stderrLines, + baseUrl: `http://127.0.0.1:${port}`, + }; +} + +async function waitForServer( + baseUrl: string, + logs: { stdoutLines: string[]; stderrLines: string[] } +) { + const startedAt = Date.now(); + let lastError = ""; + while (Date.now() - startedAt < 120_000) { + try { + const response = await fetch(`${baseUrl}/api/monitoring/health`, { + signal: AbortSignal.timeout(5_000), + }); + if (response.ok) return; + lastError = `HTTP ${response.status}`; + } catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + await sleep(500); + } + + throw new Error( + [ + `Timed out waiting for OmniRoute to start: ${lastError}`, + "--- stdout ---", + ...logs.stdoutLines.slice(-40), + "--- stderr ---", + ...logs.stderrLines.slice(-40), + ].join("\n") + ); +} + +async function stopProcess(child: ReturnType) { + if (child.killed) return; + child.kill("SIGTERM"); + const exited = await Promise.race([ + new Promise((resolve) => child.once("exit", () => resolve(true))), + sleep(5_000).then(() => false), + ]); + if (!exited && !child.killed) { + child.kill("SIGKILL"); + await new Promise((resolve) => child.once("exit", () => resolve())); + } +} + +async function seedCompatibleProvider(prefix: string, apiKey: string, baseUrl: string) { + const providerId = `openai-compatible-chat-e2e-${prefix}`; + await providersDb.createProviderNode({ + id: providerId, + type: "openai-compatible", + name: `E2E ${prefix}`, + prefix, + apiType: "chat", + baseUrl, + }); + await providersDb.createProviderConnection({ + provider: providerId, + authType: "apikey", + name: `conn-${prefix}`, + apiKey, + isActive: true, + testStatus: "active", + providerSpecificData: { + baseUrl, + apiType: "chat", + }, + }); + return { providerId, model: `${prefix}/test-model`, apiKey }; +} + +function buildResilienceConfig(overrides: Record = {}) { + const base = { + requestQueue: { + autoEnableApiKeyProviders: true, + requestsPerMinute: 120, + minTimeBetweenRequestsMs: 0, + concurrentRequests: 4, + maxWaitMs: 2_000, + }, + connectionCooldown: { + oauth: { + baseCooldownMs: 500, + useUpstreamRetryHints: true, + maxBackoffSteps: 3, + }, + apikey: { + baseCooldownMs: 300, + useUpstreamRetryHints: false, + maxBackoffSteps: 0, + }, + }, + providerBreaker: { + oauth: { + failureThreshold: 3, + resetTimeoutMs: 2_000, + }, + apikey: { + failureThreshold: 2, + resetTimeoutMs: 1_500, + }, + }, + waitForCooldown: { + enabled: false, + maxRetries: 0, + maxRetryWaitSec: 0, + }, + }; + + return { + ...base, + ...overrides, + requestQueue: { + ...base.requestQueue, + ...((overrides.requestQueue as Record) || {}), + }, + connectionCooldown: { + oauth: { + ...base.connectionCooldown.oauth, + ...(((overrides.connectionCooldown as Record)?.oauth as Record< + string, + unknown + >) || {}), + }, + apikey: { + ...base.connectionCooldown.apikey, + ...(((overrides.connectionCooldown as Record)?.apikey as Record< + string, + unknown + >) || {}), + }, + }, + providerBreaker: { + oauth: { + ...base.providerBreaker.oauth, + ...(((overrides.providerBreaker as Record)?.oauth as Record< + string, + unknown + >) || {}), + }, + apikey: { + ...base.providerBreaker.apikey, + ...(((overrides.providerBreaker as Record)?.apikey as Record< + string, + unknown + >) || {}), + }, + }, + waitForCooldown: { + ...base.waitForCooldown, + ...((overrides.waitForCooldown as Record) || {}), + }, + }; +} + +async function patchResilience(baseUrl: string, config: Record) { + const response = await fetch(`${baseUrl}/api/resilience`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(config), + signal: AbortSignal.timeout(10_000), + }); + const payload = await response.json(); + assert.equal(response.status, 200, JSON.stringify(payload)); + return payload; +} + +async function getJson(url: string) { + const response = await fetch(url, { signal: AbortSignal.timeout(10_000) }); + const json = await response.json(); + return { response, json }; +} + +async function postChat(baseUrl: string, model: string, content: string) { + const response = await fetch(`${baseUrl}/api/v1/chat/completions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model, + stream: false, + messages: [{ role: "user", content }], + }), + signal: AbortSignal.timeout(20_000), + }); + const text = await response.text(); + const json = text ? JSON.parse(text) : {}; + return { response, json }; +} + +const relay = createFakeOpenAiRelay(); +let app: + | { + child: ReturnType; + stdoutLines: string[]; + stderrLines: string[]; + baseUrl: string; + } + | undefined; + +const TOKENS = { + p1: "sk-e2e-p1", + p2: "sk-e2e-p2", + p3: "sk-e2e-p3", + p4: "sk-e2e-p4", + p5: "sk-e2e-p5", + p6: "sk-e2e-p6", + p7: "sk-e2e-p7", + p8: "sk-e2e-p8", +}; + +test.before(async () => { + const fakeBaseUrl = await relay.start(); + + relay.configureToken(TOKENS.p1, { + defaultResponse: buildCompletion("primary healthy again"), + queue: [buildError(503, "primary transient failure")], + }); + relay.configureToken(TOKENS.p2, { + defaultResponse: buildCompletion("secondary stable"), + }); + relay.configureToken(TOKENS.p3, { + defaultResponse: buildCompletion("wait-for-cooldown via upstream hint"), + queue: [buildError(429, "rate limited, retry after 1 second", { "Retry-After": "1" })], + }); + relay.configureToken(TOKENS.p4, { + defaultResponse: buildCompletion("ignored upstream retry hint"), + queue: [buildError(429, "rate limited, retry after 5 seconds", { "Retry-After": "5" })], + }); + relay.configureToken(TOKENS.p5, { + defaultResponse: buildCompletion("breaker target recovered"), + queue: [buildError(503, "breaker failure #1"), buildError(503, "breaker failure #2")], + }); + relay.configureToken(TOKENS.p6, { + defaultResponse: buildCompletion("round robin A"), + }); + relay.configureToken(TOKENS.p7, { + defaultResponse: buildCompletion("round robin B"), + }); + relay.configureToken(TOKENS.p8, { + defaultResponse: { + ...buildCompletion("queued connection request"), + delayMs: 250, + }, + }); + + await seedCompatibleProvider("p1", TOKENS.p1, fakeBaseUrl); + await seedCompatibleProvider("p2", TOKENS.p2, fakeBaseUrl); + await seedCompatibleProvider("p3", TOKENS.p3, fakeBaseUrl); + await seedCompatibleProvider("p4", TOKENS.p4, fakeBaseUrl); + await seedCompatibleProvider("p5", TOKENS.p5, fakeBaseUrl); + await seedCompatibleProvider("p6", TOKENS.p6, fakeBaseUrl); + await seedCompatibleProvider("p7", TOKENS.p7, fakeBaseUrl); + await seedCompatibleProvider("p8", TOKENS.p8, fakeBaseUrl); + + await combosDb.createCombo({ + name: "res-priority-fallback", + strategy: "priority", + config: { maxRetries: 0, retryDelayMs: 0 }, + models: ["p1/test-model", "p2/test-model"], + }); + await combosDb.createCombo({ + name: "res-breaker-combo", + strategy: "priority", + config: { maxRetries: 0, retryDelayMs: 0 }, + models: ["p5/test-model", "p2/test-model"], + }); + await combosDb.createCombo({ + name: "res-rr", + strategy: "round-robin", + config: { maxRetries: 0, retryDelayMs: 0, concurrencyPerModel: 1, queueTimeoutMs: 800 }, + models: ["p6/test-model", "p7/test-model"], + }); + + await settingsDb.updateSettings({ + resilienceSettings: buildResilienceConfig(), + requestRetry: 0, + maxRetryIntervalSec: 0, + requireLogin: false, + setupComplete: true, + }); + + core.closeDbInstance(); + + app = createServerProcess(TEST_DATA_DIR, DASHBOARD_PORT); + await waitForServer(app.baseUrl, app); + + await patchResilience(app.baseUrl, buildResilienceConfig()); + + const warmup = await postChat(app.baseUrl, "p2/test-model", "warm up chat route"); + assert.equal(warmup.response.status, 200, JSON.stringify(warmup.json)); + relay.resetState(TOKENS.p2); +}); + +test.after(async () => { + if (app) { + await stopProcess(app.child); + } + await relay.stop(); + core.closeDbInstance(); + await fsp.rm(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("resilience API only exposes configuration, not runtime breaker state", async () => { + assert.ok(app); + const { response, json } = await getJson(`${app.baseUrl}/api/resilience`); + + assert.equal(response.status, 200); + assert.deepEqual(Object.keys(json).sort(), [ + "connectionCooldown", + "legacy", + "providerBreaker", + "requestQueue", + "waitForCooldown", + ]); + assert.equal("providerBreakers" in json, false); + assert.equal("runtime" in json, false); +}); + +test("request queue serializes concurrent requests on the same connection", async () => { + assert.ok(app); + await patchResilience( + app.baseUrl, + buildResilienceConfig({ + requestQueue: { + concurrentRequests: 1, + maxWaitMs: 1_500, + }, + }) + ); + relay.resetState(TOKENS.p8); + + const startedAt = Date.now(); + const [first, second] = await Promise.all([ + postChat(app.baseUrl, "p8/test-model", "queue-one"), + postChat(app.baseUrl, "p8/test-model", "queue-two"), + ]); + const elapsed = Date.now() - startedAt; + const state = relay.getState(TOKENS.p8); + + assert.equal(first.response.status, 200, JSON.stringify(first.json)); + assert.equal(second.response.status, 200, JSON.stringify(second.json)); + assert.equal(state.hits, 2); + assert.ok(elapsed >= 450, `expected queued elapsed >= 450ms, got ${elapsed}ms`); + assert.ok( + state.startedAt[1] - state.startedAt[0] >= 180, + `expected second request to be delayed by queue, got ${state.startedAt[1] - state.startedAt[0]}ms` + ); +}); + +test("priority combo falls back on 503 and skips the cooled-down primary on the next request", async () => { + assert.ok(app); + await patchResilience(app.baseUrl, buildResilienceConfig()); + relay.resetState(TOKENS.p1, [buildError(503, "primary transient failure")]); + relay.resetState(TOKENS.p2); + + const first = await postChat(app.baseUrl, "res-priority-fallback", "priority fallback request"); + assert.equal(first.response.status, 200, JSON.stringify(first.json)); + assert.equal(first.json.choices[0].message.content, "secondary stable"); + assert.equal(relay.getState(TOKENS.p1).hits, 1); + assert.equal(relay.getState(TOKENS.p2).hits, 1); + + const second = await postChat(app.baseUrl, "res-priority-fallback", "priority fallback again"); + assert.equal(second.response.status, 200, JSON.stringify(second.json)); + assert.equal(second.json.choices[0].message.content, "secondary stable"); + assert.equal(relay.getState(TOKENS.p1).hits, 1); + assert.equal(relay.getState(TOKENS.p2).hits, 2); +}); + +test("wait-for-cooldown honors upstream Retry-After when enabled", async () => { + assert.ok(app); + await patchResilience( + app.baseUrl, + buildResilienceConfig({ + connectionCooldown: { + apikey: { + useUpstreamRetryHints: true, + baseCooldownMs: 200, + }, + }, + waitForCooldown: { + enabled: true, + maxRetries: 1, + maxRetryWaitSec: 2, + }, + }) + ); + relay.resetState(TOKENS.p3); + const warmup = await postChat(app.baseUrl, "p3/test-model", "warm provider-specific route"); + assert.equal(warmup.response.status, 200, JSON.stringify(warmup.json)); + relay.resetState(TOKENS.p3, [ + buildError(429, "rate limited, retry after 1 second", { "Retry-After": "1" }), + ]); + + const startedAt = Date.now(); + const result = await postChat(app.baseUrl, "p3/test-model", "wait for cooldown via upstream"); + const elapsed = Date.now() - startedAt; + + assert.equal(result.response.status, 200, JSON.stringify(result.json)); + assert.equal(result.json.choices[0].message.content, "wait-for-cooldown via upstream hint"); + assert.equal(relay.getState(TOKENS.p3).hits, 2); + assert.ok(elapsed >= 800, `expected upstream wait >= 800ms, got ${elapsed}ms`); +}); + +test("connection cooldown can ignore upstream Retry-After and use the configured local cooldown", async () => { + assert.ok(app); + await patchResilience( + app.baseUrl, + buildResilienceConfig({ + connectionCooldown: { + apikey: { + useUpstreamRetryHints: false, + baseCooldownMs: 200, + }, + }, + waitForCooldown: { + enabled: true, + maxRetries: 1, + maxRetryWaitSec: 2, + }, + }) + ); + relay.resetState(TOKENS.p4); + const warmup = await postChat(app.baseUrl, "p4/test-model", "warm provider-specific route"); + assert.equal(warmup.response.status, 200, JSON.stringify(warmup.json)); + relay.resetState(TOKENS.p4, [ + buildError(429, "rate limited, retry after 30 seconds", { "Retry-After": "30" }), + ]); + + const startedAt = Date.now(); + const result = await postChat(app.baseUrl, "p4/test-model", "ignore upstream retry-after"); + const elapsed = Date.now() - startedAt; + + assert.equal(result.response.status, 200, JSON.stringify(result.json)); + assert.equal(result.json.choices[0].message.content, "ignored upstream retry hint"); + assert.equal(relay.getState(TOKENS.p4).hits, 2); + assert.ok( + elapsed < 5_000, + `expected ignored upstream hint to avoid a 30s wait, got ${elapsed}ms` + ); +}); + +test("provider circuit breaker opens after repeated final failures and Health reports it", async () => { + assert.ok(app); + await patchResilience( + app.baseUrl, + buildResilienceConfig({ + waitForCooldown: { + enabled: false, + maxRetries: 0, + maxRetryWaitSec: 0, + }, + providerBreaker: { + apikey: { + failureThreshold: 2, + resetTimeoutMs: 1_500, + }, + }, + }) + ); + relay.resetState(TOKENS.p5, [ + buildError(503, "breaker failure #1"), + buildError(503, "breaker failure #2"), + ]); + + const first = await postChat(app.baseUrl, "p5/test-model", "breaker first failure"); + const second = await postChat(app.baseUrl, "p5/test-model", "breaker second failure"); + const third = await postChat(app.baseUrl, "p5/test-model", "breaker should now be open"); + + assert.equal(first.response.status, 503); + assert.equal(second.response.status, 503); + assert.equal(third.response.status, 503); + assert.match(String(second.json.error?.message || ""), /reset after/i); + assert.match(String(third.json.error?.message || ""), /circuit breaker is open/i); + assert.equal(relay.getState(TOKENS.p5).hits, 1); + + const health = await getJson(`${app.baseUrl}/api/monitoring/health`); + assert.equal(health.response.status, 200); + const breakerEntry = (health.json.providerBreakers || []).find( + (entry: Record) => entry.provider === "openai-compatible-chat-e2e-p5" + ); + assert.ok(breakerEntry, "expected provider breaker entry for p5"); + assert.equal(breakerEntry.state, "OPEN"); + assert.ok(Number(breakerEntry.failureCount) >= 2); + assert.ok(Number(breakerEntry.retryAfterMs) > 0); + assert.ok( + (health.json.providerBreakers || []).every( + (entry: Record) => !String(entry.provider || "").includes(":") + ) + ); +}); + +test("combo respects the global provider breaker and falls through without a combo-local breaker", async () => { + assert.ok(app); + relay.resetState(TOKENS.p2); + + const result = await postChat( + app.baseUrl, + "res-breaker-combo", + "combo should skip broken provider" + ); + + assert.equal(result.response.status, 200, JSON.stringify(result.json)); + assert.equal(result.json.choices[0].message.content, "secondary stable"); + assert.equal(relay.getState(TOKENS.p5).hits, 1); + assert.equal(relay.getState(TOKENS.p2).hits, 1); +}); + +test("round-robin combo still alternates healthy providers after combo breaker removal", async () => { + assert.ok(app); + await patchResilience(app.baseUrl, buildResilienceConfig()); + relay.resetState(TOKENS.p6); + relay.resetState(TOKENS.p7); + + const first = await postChat(app.baseUrl, "res-rr", "round robin one"); + const second = await postChat(app.baseUrl, "res-rr", "round robin two"); + + assert.equal(first.response.status, 200, JSON.stringify(first.json)); + assert.equal(second.response.status, 200, JSON.stringify(second.json)); + assert.equal(first.json.choices[0].message.content, "round robin A"); + assert.equal(second.json.choices[0].message.content, "round robin B"); + assert.equal(relay.getState(TOKENS.p6).hits, 1); + assert.equal(relay.getState(TOKENS.p7).hits, 1); +}); diff --git a/tests/integration/security-hardening.test.ts b/tests/integration/security-hardening.test.ts index df01b34c2f..cb6060e247 100644 --- a/tests/integration/security-hardening.test.ts +++ b/tests/integration/security-hardening.test.ts @@ -232,7 +232,6 @@ test("T06 route payload validation uses validateBody in critical endpoints", () "src/app/api/policies/route.ts", "src/app/api/fallback/chains/route.ts", "src/app/api/models/route.ts", - "src/app/api/models/availability/route.ts", "src/app/api/provider-models/route.ts", "src/app/api/pricing/route.ts", "src/app/api/rate-limits/route.ts", diff --git a/tests/unit/account-fallback-service.test.ts b/tests/unit/account-fallback-service.test.ts index 4f10e7e0a8..2095057770 100644 --- a/tests/unit/account-fallback-service.test.ts +++ b/tests/unit/account-fallback-service.test.ts @@ -22,6 +22,12 @@ const { recordModelLockoutFailure, clearModelLock, shouldMarkAccountExhaustedFrom429, + recordProviderFailure, + isProviderInCooldown, + getProviderCooldownRemainingMs, + clearProviderFailure, + isProviderFailureCode, + getProvidersInCooldown, } = accountFallback; const { selectAccount } = accountSelector; @@ -201,8 +207,37 @@ test("lockModelIfPerModelQuota only locks supported providers and real models", }); test("getProviderProfile differentiates oauth and api-key providers", () => { - assert.deepEqual(getProviderProfile("claude"), PROVIDER_PROFILES.oauth); - assert.deepEqual(getProviderProfile("openai"), PROVIDER_PROFILES.apikey); + const oauthProfile = getProviderProfile("claude"); + assert.equal(oauthProfile.transientCooldown, PROVIDER_PROFILES.oauth.transientCooldown); + assert.equal( + oauthProfile.rateLimitCooldown, + oauthProfile.useUpstreamRetryHints ? 0 : oauthProfile.baseCooldownMs + ); + assert.equal(oauthProfile.maxBackoffLevel, PROVIDER_PROFILES.oauth.maxBackoffLevel); + assert.equal( + oauthProfile.circuitBreakerThreshold, + PROVIDER_PROFILES.oauth.circuitBreakerThreshold + ); + assert.equal(oauthProfile.circuitBreakerReset, PROVIDER_PROFILES.oauth.circuitBreakerReset); + assert.equal(oauthProfile.baseCooldownMs, PROVIDER_PROFILES.oauth.transientCooldown); + assert.equal(oauthProfile.failureThreshold, PROVIDER_PROFILES.oauth.circuitBreakerThreshold); + assert.equal(oauthProfile.resetTimeoutMs, PROVIDER_PROFILES.oauth.circuitBreakerReset); + + const apiKeyProfile = getProviderProfile("openai"); + assert.equal(apiKeyProfile.transientCooldown, PROVIDER_PROFILES.apikey.transientCooldown); + assert.equal( + apiKeyProfile.rateLimitCooldown, + apiKeyProfile.useUpstreamRetryHints ? 0 : apiKeyProfile.baseCooldownMs + ); + assert.equal(apiKeyProfile.maxBackoffLevel, PROVIDER_PROFILES.apikey.maxBackoffLevel); + assert.equal( + apiKeyProfile.circuitBreakerThreshold, + PROVIDER_PROFILES.apikey.circuitBreakerThreshold + ); + assert.equal(apiKeyProfile.circuitBreakerReset, PROVIDER_PROFILES.apikey.circuitBreakerReset); + assert.equal(apiKeyProfile.baseCooldownMs, PROVIDER_PROFILES.apikey.transientCooldown); + assert.equal(apiKeyProfile.failureThreshold, PROVIDER_PROFILES.apikey.circuitBreakerThreshold); + assert.equal(apiKeyProfile.resetTimeoutMs, PROVIDER_PROFILES.apikey.circuitBreakerReset); }); test("shouldMarkAccountExhaustedFrom429 skips connection poisoning for compatible providers", () => { @@ -223,11 +258,11 @@ test("recordModelLockoutFailure uses provider profile cooldowns, backoff, and re const compatibleProvider = "openai-compatible-custom-node"; const compatibleModel = "custom-model-a"; const profile = { - transientCooldown: 250, - rateLimitCooldown: 125, - maxBackoffLevel: 2, - circuitBreakerThreshold: 60, - circuitBreakerReset: 500, + baseCooldownMs: 125, + useUpstreamRetryHints: false, + maxBackoffSteps: 2, + failureThreshold: 60, + resetTimeoutMs: 500, }; const first = recordModelLockoutFailure( @@ -290,3 +325,169 @@ test("recordModelLockoutFailure uses provider profile cooldowns, backoff, and re clearModelLock("openai-compatible-custom-node", "conn-compatible", "custom-model-a"); } }); + +// Provider-level failure circuit breaker tests +test("isProviderFailureCode correctly identifies provider-wide transient error codes", () => { + assert.equal(isProviderFailureCode(429), false); + assert.equal(isProviderFailureCode(408), true); + assert.equal(isProviderFailureCode(500), true); + assert.equal(isProviderFailureCode(502), true); + assert.equal(isProviderFailureCode(503), true); + assert.equal(isProviderFailureCode(504), true); + assert.equal(isProviderFailureCode(401), false); + assert.equal(isProviderFailureCode(403), false); + assert.equal(isProviderFailureCode(400), false); + assert.equal(isProviderFailureCode(404), false); + assert.equal(isProviderFailureCode(200), false); +}); + +test("recordProviderFailure tracks failures and triggers cooldown after threshold", () => { + const originalNow = Date.now; + let now = 1_700_000_000_000; + Date.now = () => now; + + try { + const provider = "test-provider"; + + // Clear any existing state + clearProviderFailure(provider); + assert.equal(isProviderInCooldown(provider), false); + assert.equal(getProviderCooldownRemainingMs(provider), null); + + // Record 4 failures - should not trigger cooldown yet + for (let i = 0; i < 4; i++) { + recordProviderFailure(provider); + now += 1000; // 1 second between failures + } + assert.equal(isProviderInCooldown(provider), false); + + // 5th failure - should trigger cooldown + recordProviderFailure(provider); + assert.equal(isProviderInCooldown(provider), true); + + const remaining = getProviderCooldownRemainingMs(provider); + assert.ok(remaining !== null); + assert.ok(remaining > 0); + assert.ok(remaining <= 10 * 60 * 1000); // 10 minutes max + + // Check getProvidersInCooldown returns the provider + const inCooldown = getProvidersInCooldown(); + assert.ok(inCooldown.some((p) => p.provider === provider)); + assert.equal(inCooldown.find((p) => p.provider === provider)?.failureCount, 5); + + // Simulate cooldown expiration + now += 11 * 60 * 1000; // 11 minutes later + assert.equal(isProviderInCooldown(provider), false); + assert.equal(getProviderCooldownRemainingMs(provider), null); + assert.equal( + getProvidersInCooldown().some((p) => p.provider === provider), + false + ); + } finally { + Date.now = originalNow; + clearProviderFailure("test-provider"); + } +}); + +test("checkFallbackError no longer mutates provider breaker state on per-connection failures", () => { + const provider = "test-provider-check"; + clearProviderFailure(provider); + + for (let i = 0; i < 5; i++) { + checkFallbackError(429, "rate limited", 0, null, provider); + } + + assert.equal(isProviderInCooldown(provider), false); + clearProviderFailure(provider); +}); + +test("checkFallbackError does not record provider failure for non-transient errors", () => { + const originalNow = Date.now; + let now = 1_700_000_000_000; + Date.now = () => now; + + try { + const provider = "test-provider-no-record"; + clearProviderFailure(provider); + + // Simulate 5 auth errors (401) - should NOT trigger provider cooldown + for (let i = 0; i < 5; i++) { + checkFallbackError(401, "unauthorized", 0, null, provider); + now += 1000; + } + + // Provider should NOT be in cooldown + assert.equal(isProviderInCooldown(provider), false); + } finally { + Date.now = originalNow; + clearProviderFailure("test-provider-no-record"); + } +}); + +test("clearProviderFailure removes provider from cooldown", () => { + const originalNow = Date.now; + let now = 1_700_000_000_000; + Date.now = () => now; + + try { + const provider = "test-provider-clear"; + clearProviderFailure(provider); + + // Trigger cooldown + for (let i = 0; i < 5; i++) { + recordProviderFailure(provider); + now += 1000; + } + assert.equal(isProviderInCooldown(provider), true); + + // Clear the failure state + clearProviderFailure(provider); + assert.equal(isProviderInCooldown(provider), false); + assert.equal(getProviderCooldownRemainingMs(provider), null); + } finally { + Date.now = originalNow; + clearProviderFailure("test-provider-clear"); + } +}); + +// Daily quota exhausted detection tests +test("isDailyQuotaExhausted detects today's quota errors", () => { + const { isDailyQuotaExhausted } = accountFallback; + assert.equal(isDailyQuotaExhausted("You have exceeded today's quota for model X"), true); + assert.equal(isDailyQuotaExhausted("exceeded your daily quota"), true); + assert.equal(isDailyQuotaExhausted("Please try again tomorrow"), true); + assert.equal(isDailyQuotaExhausted("rate limit exceeded"), false); + assert.equal(isDailyQuotaExhausted(""), false); + assert.equal(isDailyQuotaExhausted(null), false); +}); + +test("getMsUntilTomorrow returns positive value less than 24 hours", () => { + const { getMsUntilTomorrow } = accountFallback; + const ms = getMsUntilTomorrow(); + assert.ok(ms > 0, "should be positive"); + assert.ok(ms <= 24 * 60 * 60 * 1000, "should be <= 24 hours"); +}); + +test("checkFallbackError locks model until tomorrow for daily quota exhaustion", () => { + const result = checkFallbackError( + 429, + "You have exceeded today's quota for model moonshotai/Kimi-K2.5, please try again tomorrow" + ); + assert.equal(result.shouldFallback, true); + assert.equal(result.reason, RateLimitReason.QUOTA_EXHAUSTED); + assert.equal(result.dailyQuotaExhausted, true); + assert.ok(result.cooldownMs > 0, "cooldown should be positive"); + assert.ok(result.cooldownMs <= 24 * 60 * 60 * 1000, "cooldown should be <= 24 hours"); +}); + +test("checkFallbackError detects daily quota with 'try again tomorrow'", () => { + const result = checkFallbackError(429, "Please try again tomorrow"); + assert.equal(result.shouldFallback, true); + assert.equal(result.dailyQuotaExhausted, true); +}); + +test("checkFallbackError detects daily quota with 'daily quota'", () => { + const result = checkFallbackError(429, "You have exceeded your daily quota"); + assert.equal(result.shouldFallback, true); + assert.equal(result.dailyQuotaExhausted, true); +}); diff --git a/tests/unit/auth-terminal-status.test.ts b/tests/unit/auth-terminal-status.test.ts index a62c4ba788..e3cad8eea0 100644 --- a/tests/unit/auth-terminal-status.test.ts +++ b/tests/unit/auth-terminal-status.test.ts @@ -109,3 +109,129 @@ test("markAccountUnavailable does not overwrite terminal status", async () => { const after = await providersDb.getProviderConnectionById(conn.id); assert.equal(after.testStatus, "credits_exhausted"); }); + +test("markAccountUnavailable marks 401 connections as expired without adding cooldown", async () => { + await resetStorage(); + + const conn = await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + apiKey: "sk-expired", + isActive: true, + testStatus: "active", + }); + + const result = await auth.markAccountUnavailable( + conn.id, + 401, + "unauthorized", + "openai", + "gpt-4.1" + ); + const after = await providersDb.getProviderConnectionById(conn.id); + + assert.equal(result.shouldFallback, true); + assert.equal(result.cooldownMs, 0); + assert.equal(after.testStatus, "expired"); + assert.ok(!after.rateLimitedUntil); +}); + +test("markAccountUnavailable marks 402 connections as credits_exhausted without adding cooldown", async () => { + await resetStorage(); + + const conn = await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + apiKey: "sk-credits", + isActive: true, + testStatus: "active", + }); + + const result = await auth.markAccountUnavailable( + conn.id, + 402, + "payment required", + "openai", + "gpt-4.1" + ); + const after = await providersDb.getProviderConnectionById(conn.id); + + assert.equal(result.shouldFallback, true); + assert.equal(result.cooldownMs, 0); + assert.equal(after.testStatus, "credits_exhausted"); + assert.ok(!after.rateLimitedUntil); +}); + +test("markAccountUnavailable marks 403 connections as banned without adding cooldown", async () => { + await resetStorage(); + + const conn = await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + apiKey: "sk-banned", + isActive: true, + testStatus: "active", + }); + + const result = await auth.markAccountUnavailable(conn.id, 403, "forbidden", "openai", "gpt-4.1"); + const after = await providersDb.getProviderConnectionById(conn.id); + + assert.equal(result.shouldFallback, true); + assert.equal(result.cooldownMs, 0); + assert.equal(after.testStatus, "banned"); + assert.ok(!after.rateLimitedUntil); +}); + +test("markAccountUnavailable keeps project-route 403 errors non-terminal", async () => { + await resetStorage(); + + const conn = await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + apiKey: "sk-project-route", + isActive: true, + testStatus: "active", + }); + + const result = await auth.markAccountUnavailable( + conn.id, + 403, + "The service has not been used in project", + "openai", + "gpt-4.1" + ); + const after = await providersDb.getProviderConnectionById(conn.id); + + assert.equal(result.shouldFallback, true); + assert.equal(result.cooldownMs, 0); + assert.equal(after.testStatus, "active"); + assert.equal(after.lastErrorType, "project_route_error"); + assert.ok(!after.rateLimitedUntil); +}); + +test("markAccountUnavailable keeps oauth-invalid 401 errors non-terminal", async () => { + await resetStorage(); + + const conn = await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + apiKey: "sk-oauth-invalid", + isActive: true, + testStatus: "active", + }); + + const result = await auth.markAccountUnavailable( + conn.id, + 401, + "Invalid authentication credentials provided", + "openai", + "gpt-4.1" + ); + const after = await providersDb.getProviderConnectionById(conn.id); + + assert.equal(result.shouldFallback, true); + assert.equal(result.cooldownMs, 0); + assert.equal(after.testStatus, "active"); + assert.equal(after.lastErrorType, "oauth_invalid_token"); + assert.ok(!after.rateLimitedUntil); +}); diff --git a/tests/unit/autocombo-unification.test.ts b/tests/unit/autocombo-unification.test.ts index a622651fdc..4ff04fa2dc 100644 --- a/tests/unit/autocombo-unification.test.ts +++ b/tests/unit/autocombo-unification.test.ts @@ -128,29 +128,40 @@ test("sidebar visibility excludes the removed auto-combo item", async () => { assert.deepEqual(sidebarVisibility.normalizeHiddenSidebarItems(["auto-combo", "home"]), ["home"]); }); -test("intelligent routing helpers normalize config and health state", () => { +test("intelligent routing helpers normalize config and build provider scores", () => { const normalizedConfig = intelligentRouting.normalizeIntelligentRoutingConfig({ candidatePool: ["openai", "anthropic"], explorationRate: "0.25", + modePack: "", + routerStrategy: "", weights: { quota: 0.4 }, }); assert.deepEqual(normalizedConfig.candidatePool, ["openai", "anthropic"]); assert.equal(normalizedConfig.explorationRate, 0.25); + assert.equal(normalizedConfig.modePack, "ship-fast"); + assert.equal(normalizedConfig.routerStrategy, "rules"); assert.equal(normalizedConfig.weights.quota, 0.4); assert.equal( normalizedConfig.weights.health, intelligentRouting.DEFAULT_INTELLIGENT_WEIGHTS.health ); - const healthState = intelligentRouting.extractIntelligentHealthState({ - circuitBreakers: [ - { provider: "openai", state: "OPEN", lastFailure: "2026-04-12T12:00:00Z" }, - { provider: "anthropic", state: "OPEN", lastFailure: "2026-04-12T12:01:00Z" }, - { provider: "google", state: "CLOSED" }, - ], + const providerScores = intelligentRouting.buildIntelligentProviderScores({ + config: normalizedConfig, }); - assert.equal(healthState.incidentMode, true); - assert.equal(healthState.exclusions.length, 2); + assert.equal(providerScores.length, 2); + assert.deepEqual( + providerScores.map((entry) => ({ + provider: entry.provider, + model: entry.model, + score: entry.score, + quotaWeight: entry.factors.quota, + })), + [ + { provider: "openai", model: "auto", score: 0.5, quotaWeight: 0.4 }, + { provider: "anthropic", model: "auto", score: 0.5, quotaWeight: 0.4 }, + ] + ); }); diff --git a/tests/unit/batch-a-domain.test.ts b/tests/unit/batch-a-domain.test.ts index 5efb40bf48..fd7e1237b7 100644 --- a/tests/unit/batch-a-domain.test.ts +++ b/tests/unit/batch-a-domain.test.ts @@ -1,166 +1,13 @@ /** * Batch A — Domain Layer + Infrastructure Tests * - * Tests for: modelAvailability, costRules, fallbackPolicy, + * Tests for: costRules, fallbackPolicy, * errorCodes, requestId, fetchTimeout */ import { describe, it, before, after } from "node:test"; import assert from "node:assert/strict"; -// ──────────────── T-19: Model Availability ──────────────── - -import { - isModelAvailable, - setModelUnavailable, - markModelAsProblematic, - clearModelUnavailability, - getAvailabilityReport, - getUnavailableCount, - resetAllAvailability, -} from "../../src/domain/modelAvailability.ts"; - -describe("modelAvailability", () => { - before(() => resetAllAvailability()); - after(() => resetAllAvailability()); - - it("should report model as available by default", () => { - assert.equal(isModelAvailable("openai", "gpt-4o"), true); - }); - - it("should mark model as unavailable", () => { - setModelUnavailable("openai", "gpt-4o", 60000, "rate limited"); - assert.equal(isModelAvailable("openai", "gpt-4o"), false); - }); - - it("should report unavailable models", () => { - const report = getAvailabilityReport(); - assert.equal(report.length, 1); - assert.equal(report[0].provider, "openai"); - assert.equal(report[0].model, "gpt-4o"); - assert.equal(report[0].reason, "rate limited"); - assert.ok(report[0].remainingMs > 0); - }); - - it("should count unavailable models", () => { - assert.equal(getUnavailableCount(), 1); - }); - - it("should clear model unavailability", () => { - clearModelUnavailability("openai", "gpt-4o"); - assert.equal(isModelAvailable("openai", "gpt-4o"), true); - assert.equal(getUnavailableCount(), 0); - }); - - it("should auto-expire after cooldown", () => { - setModelUnavailable("anthropic", "claude-sonnet-4-20250514", 1, "test"); - // Wait 2ms for expiry - const start = Date.now(); - while (Date.now() - start < 5) {} // spin wait - assert.equal(isModelAvailable("anthropic", "claude-sonnet-4-20250514"), true); - }); - - it("should apply adaptive quarantine for repeated problematic failures", () => { - const first = markModelAsProblematic("nvidia", "z-ai/glm4.7", { - status: 502, - baseCooldownMs: 3000, - reason: "HTTP 502", - }); - const second = markModelAsProblematic("nvidia", "z-ai/glm4.7", { - status: 502, - baseCooldownMs: 3000, - reason: "HTTP 502", - }); - - assert.equal(first.failureCount, 1); - assert.equal(first.cooldownMs, 120000); - assert.equal(second.failureCount, 2); - assert.equal(second.cooldownMs, 240000); - - const report = getAvailabilityReport(); - const entry = report.find((r) => r.provider === "nvidia" && r.model === "z-ai/glm4.7"); - assert.ok(entry, "nvidia model should be quarantined"); - assert.ok(entry.remainingMs >= 200000, "cooldown should be preserved/escalated"); - }); - - it("should reset failure history after model recovery", () => { - clearModelUnavailability("nvidia", "z-ai/glm4.7"); - const afterReset = markModelAsProblematic("nvidia", "z-ai/glm4.7", { - status: 502, - baseCooldownMs: 3000, - reason: "HTTP 502", - }); - - assert.equal(afterReset.failureCount, 1); - assert.equal(afterReset.cooldownMs, 120000); - }); - - it("should use provider profile threshold, cooldowns, and reset window for global quarantine", () => { - const originalNow = Date.now; - let now = 10_000; - Date.now = () => now; - - try { - const profile = { - transientCooldown: 200, - rateLimitCooldown: 100, - maxBackoffLevel: 2, - circuitBreakerThreshold: 3, - circuitBreakerReset: 500, - }; - - const first = markModelAsProblematic("openai", "gpt-4o-mini", { - status: 429, - reason: "HTTP 429", - profile, - }); - const second = markModelAsProblematic("openai", "gpt-4o-mini", { - status: 429, - reason: "HTTP 429", - profile, - }); - - assert.equal(first.failureCount, 1); - assert.equal(first.cooldownMs, 100); - assert.equal(first.quarantined, false); - assert.equal(isModelAvailable("openai", "gpt-4o-mini"), true); - - assert.equal(second.failureCount, 2); - assert.equal(second.cooldownMs, 200); - assert.equal(second.quarantined, false); - assert.equal(isModelAvailable("openai", "gpt-4o-mini"), true); - - now += 10; - const third = markModelAsProblematic("openai", "gpt-4o-mini", { - status: 429, - reason: "HTTP 429", - profile, - }); - - assert.equal(third.failureCount, 3); - assert.equal(third.cooldownMs, 400); - assert.equal(third.quarantined, true); - assert.equal(isModelAvailable("openai", "gpt-4o-mini"), false); - - now += 600; - assert.equal(isModelAvailable("openai", "gpt-4o-mini"), true); - - const afterWindow = markModelAsProblematic("openai", "gpt-4o-mini", { - status: 429, - reason: "HTTP 429", - profile, - }); - - assert.equal(afterWindow.failureCount, 1); - assert.equal(afterWindow.cooldownMs, 100); - assert.equal(afterWindow.quarantined, false); - } finally { - Date.now = originalNow; - clearModelUnavailability("openai", "gpt-4o-mini"); - } - }); -}); - // ──────────────── T-19: Cost Rules ──────────────── import { diff --git a/tests/unit/chat-combo-live-test.test.ts b/tests/unit/chat-combo-live-test.test.ts index 107d601ccd..31c99f7517 100644 --- a/tests/unit/chat-combo-live-test.test.ts +++ b/tests/unit/chat-combo-live-test.test.ts @@ -12,8 +12,6 @@ const providersDb = await import("../../src/lib/db/providers.ts"); const chatRoute = await import("../../src/app/api/v1/chat/completions/route.ts"); const { generateSignature, invalidateBySignature, setCachedResponse } = await import("../../src/lib/semanticCache.ts"); -const { clearModelUnavailability, resetAllAvailability, setModelUnavailable } = - await import("../../src/domain/modelAvailability.ts"); const { getCircuitBreaker, resetAllCircuitBreakers, STATE } = await import("../../src/shared/utils/circuitBreaker.ts"); @@ -28,7 +26,6 @@ async function resetStorage() { core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); - resetAllAvailability(); resetAllCircuitBreakers(); } @@ -79,27 +76,20 @@ test.beforeEach(async () => { test.afterEach(async () => { await flushBackgroundWork(); globalThis.fetch = originalFetch; - resetAllAvailability(); resetAllCircuitBreakers(); }); test.after(async () => { await flushBackgroundWork(); globalThis.fetch = originalFetch; - resetAllAvailability(); resetAllCircuitBreakers(); core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); -test("combo live test bypasses local cooldown and breaker state to perform a real upstream request", async () => { +test("combo live test bypasses connection cooldown and breaker state to perform a real upstream request", async () => { const created = await seedSuppressedConnection(); - setModelUnavailable("openai", "gpt-4o-mini", 60_000, "test cooldown"); - const breaker = getCircuitBreaker("openai"); - breaker.state = STATE.OPEN; - breaker.lastFailureTime = Date.now(); - const fetchCalls = []; globalThis.fetch = async (url, init = {}) => { fetchCalls.push({ url: String(url), init }); @@ -120,7 +110,10 @@ test("combo live test bypasses local cooldown and breaker state to perform a rea assert.equal(blockedByCooldown.status, 503); assert.equal(fetchCalls.length, 0); - clearModelUnavailability("openai", "gpt-4o-mini"); + const breaker = getCircuitBreaker("openai"); + breaker.state = STATE.OPEN; + breaker.lastFailureTime = Date.now(); + breaker.resetTimeout = 60_000; const blockedByBreaker = await chatRoute.POST(makeRequest()); assert.equal(blockedByBreaker.status, 503); diff --git a/tests/unit/chat-helpers.test.ts b/tests/unit/chat-helpers.test.ts index 64726e297d..102ccfd84c 100644 --- a/tests/unit/chat-helpers.test.ts +++ b/tests/unit/chat-helpers.test.ts @@ -18,13 +18,10 @@ const { safeLogEvents, withSessionHeader, } = await import("../../src/sse/handlers/chatHelpers.ts"); -const { getModelCooldownInfo, setModelUnavailable, resetAllAvailability } = - await import("../../src/domain/modelAvailability.ts"); -const { getCircuitBreaker, resetAllCircuitBreakers, CircuitBreakerOpenError, STATE } = +const { getCircuitBreaker, resetAllCircuitBreakers, STATE } = await import("../../src/shared/utils/circuitBreaker.ts"); async function resetStorage() { - resetAllAvailability(); resetAllCircuitBreakers(); core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); @@ -92,22 +89,6 @@ test("resolveModelOrError rejects malformed model strings", async () => { assert.match(json.error.message, /Invalid model format/i); }); -test("checkPipelineGates blocks models in cooldown", async () => { - setModelUnavailable("openai", "gpt-4o-mini", 12_000, "cooldown"); - - const response = await checkPipelineGates("openai", "gpt-4o-mini"); - const json = await response.json(); - const cooldownInfo = getModelCooldownInfo("openai", "gpt-4o-mini"); - const retryAfter = Number(response.headers.get("Retry-After")); - - assert.equal(response.status, 503); - assert.ok(cooldownInfo); - assert.ok(retryAfter >= 1); - assert.ok(retryAfter <= 12); - assert.ok(retryAfter >= Math.ceil((cooldownInfo?.remainingMs || 0) / 1000) - 1); - assert.match(json.error.message, /temporarily unavailable/i); -}); - test("checkPipelineGates blocks providers with an open circuit breaker", async () => { const breaker = getCircuitBreaker("openai"); breaker.state = STATE.OPEN; @@ -116,8 +97,8 @@ test("checkPipelineGates blocks providers with an open circuit breaker", async ( const response = await checkPipelineGates("openai", "gpt-4o-mini", { providerProfile: { - circuitBreakerThreshold: 5, - circuitBreakerReset: 5_000, + failureThreshold: 5, + resetTimeoutMs: 5_000, }, }); const json = await response.json(); @@ -127,6 +108,8 @@ test("checkPipelineGates blocks providers with an open circuit breaker", async ( assert.ok(retryAfter >= 4); assert.ok(retryAfter <= 5); assert.match(json.error.message, /circuit breaker is open/i); + assert.equal(json.error.code, "provider_circuit_open"); + assert.equal(response.headers.get("X-OmniRoute-Provider-Breaker"), "open"); }); test("checkPipelineGates reapplies runtime breaker settings to existing breakers", async () => { @@ -139,8 +122,8 @@ test("checkPipelineGates reapplies runtime breaker settings to existing breakers const response = await checkPipelineGates("openai", "gpt-4o-mini", { providerProfile: { - circuitBreakerThreshold: 60, - circuitBreakerReset: 5_000, + failureThreshold: 60, + resetTimeoutMs: 5_000, }, }); @@ -233,60 +216,44 @@ test("safeResolveProxy returns the direct route when no proxy config is present" }); }); -test("executeChatWithBreaker converts circuit-open and proxy-fast-fail errors", async () => { - const credentials = { connectionId: "conn_helper" }; - const openResult = await executeChatWithBreaker({ - bypassCircuitBreaker: false, - breaker: { - execute: async () => { - throw new CircuitBreakerOpenError("already open", "openai", 5_000); - }, - }, - body: { model: "openai/gpt-4o-mini" }, - provider: "openai", - model: "gpt-4o-mini", - refreshedCredentials: credentials, - proxyInfo: null, - log: console, - clientRawRequest: null, - credentials, - apiKeyInfo: null, - userAgent: "", - comboName: null, - comboStrategy: null, - isCombo: false, - extendedContext: false, - }); +test("executeChatWithBreaker converts proxy fast-fail errors", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => { + const error = new Error("Proxy unreachable"); + (error as Error & { code?: string }).code = "PROXY_UNREACHABLE"; + throw error; + }; - const proxyResult = await executeChatWithBreaker({ - bypassCircuitBreaker: false, - breaker: { - execute: async () => { - const error = new Error("Proxy unreachable"); - error.code = "PROXY_UNREACHABLE"; - throw error; - }, - }, - body: { model: "openai/gpt-4o-mini" }, - provider: "openai", - model: "gpt-4o-mini", - refreshedCredentials: credentials, - proxyInfo: null, - log: console, - clientRawRequest: null, - credentials, - apiKeyInfo: null, - userAgent: "", - comboName: null, - comboStrategy: null, - isCombo: false, - extendedContext: false, - }); + try { + const credentials = { + connectionId: "conn_helper", + apiKey: "sk-openai-helper", + providerSpecificData: {}, + }; + const proxyResult = await executeChatWithBreaker({ + body: { model: "openai/gpt-4o-mini" }, + provider: "openai", + model: "gpt-4o-mini", + refreshedCredentials: credentials, + proxyInfo: null, + log: console, + clientRawRequest: null, + credentials, + apiKeyInfo: null, + userAgent: "", + comboName: null, + comboStrategy: null, + isCombo: false, + extendedContext: false, + comboStepId: null, + comboExecutionKey: null, + }); - assert.equal(openResult.result.status, 503); - assert.equal(openResult.result.response.status, 503); - assert.equal(proxyResult.result.status, 503); - assert.equal(proxyResult.result.error, "Proxy unreachable"); + assert.equal(proxyResult.result.status, 502); + assert.match(String(proxyResult.result.error || ""), /Proxy unreachable/); + } finally { + globalThis.fetch = originalFetch; + } }); test("safeLogEvents tolerates success and timeout payloads", () => { diff --git a/tests/unit/chat-route-coverage.test.ts b/tests/unit/chat-route-coverage.test.ts index 7473e98e28..f38085ff94 100644 --- a/tests/unit/chat-route-coverage.test.ts +++ b/tests/unit/chat-route-coverage.test.ts @@ -14,13 +14,12 @@ const { resetStorage, seedApiKey, seedConnection, - setModelUnavailable, settingsDb, toPlainHeaders, } = harness; const { getCircuitBreaker, STATE } = await import("../../src/shared/utils/circuitBreaker.ts"); -const { clearModelUnavailability } = await import("../../src/domain/modelAvailability.ts"); +const { clearProviderFailure } = await import("../../open-sse/services/accountFallback.ts"); const { getDefaultTaskModelMap, resetTaskRoutingStats, setTaskRoutingConfig } = await import("../../open-sse/services/taskAwareRouter.ts"); @@ -344,9 +343,11 @@ test("handleChat returns 400 when no provider credentials exist", async () => { assert.match(json.error.message, /No credentials for provider: openai/); }); -test("handleChat returns 503 for cooled-down models and open circuit breakers", async () => { - await seedConnection("openai", { apiKey: "sk-openai-breaker" }); - setModelUnavailable("openai", "gpt-4o-mini", 60_000, "test cooldown"); +test("handleChat returns 503 for cooled-down connections and 503 for open circuit breakers", async () => { + await seedConnection("openai", { + apiKey: "sk-openai-breaker", + rateLimitedUntil: new Date(Date.now() + 60_000).toISOString(), + }); const cooldownResponse = await handleChat( buildRequest({ @@ -359,15 +360,13 @@ test("handleChat returns 503 for cooled-down models and open circuit breakers", ); const cooldownJson = await cooldownResponse.json(); assert.equal(cooldownResponse.status, 503); - assert.match(cooldownJson.error.message, /temporarily unavailable/i); - - clearModelUnavailability("openai", "gpt-4o-mini"); - const freshBreaker = getCircuitBreaker("openai"); - freshBreaker.reset(); + assert.ok(Number(cooldownResponse.headers.get("Retry-After")) >= 1); + assert.match(cooldownJson.error.message, /\[openai\/gpt-4o-mini\]/i); const breaker = getCircuitBreaker("openai"); breaker.state = STATE.OPEN; breaker.lastFailureTime = Date.now(); + breaker.resetTimeout = 60_000; const breakerBlocked = await handleChat( buildRequest({ @@ -381,6 +380,8 @@ test("handleChat returns 503 for cooled-down models and open circuit breakers", const breakerJson = await breakerBlocked.json(); assert.equal(breakerBlocked.status, 503); + assert.equal(breakerBlocked.headers.get("X-OmniRoute-Provider-Breaker"), "open"); + assert.equal(breakerJson.error.code, "provider_circuit_open"); assert.match(breakerJson.error.message, /circuit breaker is open/i); }); @@ -409,6 +410,8 @@ test("handleChat maps upstream timeouts to HTTP 504", async () => { }); test("handleChat uses the emergency fallback model on budget exhaustion", async () => { + // Reset provider failure state to avoid circuit breaker interference + clearProviderFailure("openai"); await seedConnection("openai", { apiKey: "sk-openai-billing" }); await seedConnection("nvidia", { apiKey: "sk-nvidia-fallback" }); const seenBodies = []; @@ -448,6 +451,8 @@ test("handleChat uses the emergency fallback model on budget exhaustion", async }); test("handleChat returns the primary budget error when emergency fallback also fails", async () => { + // Reset provider failure state to avoid circuit breaker interference + clearProviderFailure("openai"); await seedConnection("openai", { apiKey: "sk-openai-billing-fail" }); await seedConnection("nvidia", { apiKey: "sk-nvidia-fallback-fail" }); const seenModels = []; diff --git a/tests/unit/combo-circuit-breaker.test.ts b/tests/unit/combo-circuit-breaker.test.ts deleted file mode 100644 index 5b06a3a848..0000000000 --- a/tests/unit/combo-circuit-breaker.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; - -// Reset circuit breaker registry between tests -const { CircuitBreaker, getCircuitBreaker, getAllCircuitBreakerStatuses, STATE } = - await import("../../src/shared/utils/circuitBreaker.ts"); - -const { handleComboChat, getComboFromData } = await import("../../open-sse/services/combo.ts"); -const { normalizeComboStep } = await import("../../src/lib/combos/steps.ts"); - -const { PROVIDER_PROFILES } = await import("../../open-sse/config/constants.ts"); - -// ─── Helpers ──────────────────────────────────────────────────────────────── - -/** Create a mock logger */ -function mockLog() { - const entries = []; - return { - info: (tag, msg) => entries.push({ level: "info", tag, msg }), - warn: (tag, msg) => entries.push({ level: "warn", tag, msg }), - error: (tag, msg) => entries.push({ level: "error", tag, msg }), - entries, - }; -} - -/** Create a handleSingleModel that returns given status codes in sequence */ -function mockHandler(statusSequence) { - let callIndex = 0; - return async (body, modelStr) => { - const status = statusSequence[callIndex] ?? statusSequence[statusSequence.length - 1] ?? 200; - callIndex++; - if (status === 200) { - return new Response(JSON.stringify({ ok: true }), { status: 200 }); - } - return new Response(JSON.stringify({ error: { message: `Error ${status}` } }), { - status, - statusText: `Error ${status}`, - }); - }; -} - -function getComboTargetBreakerKey(combo, index = 0) { - const step = normalizeComboStep(combo.models[index], { - comboName: combo.name, - index, - }); - return `combo:${combo.name}:${step.id}`; -} - -// ─── Circuit Breaker Integration Tests ────────────────────────────────────── -// NOTE: combo.ts uses the full model string (e.g. "combo:groq/llama-3.3-70b") -// as the circuit breaker key, not just the provider prefix. - -test("handleComboChat: circuit breaker opens after repeated 502 errors", async () => { - const combo = { - name: "test-combo", - models: [{ model: "groq/llama-3.3-70b", weight: 0 }], - strategy: "priority", - config: { maxRetries: 0 }, - }; - const breakerKey = getComboTargetBreakerKey(combo); - const breaker = getCircuitBreaker(breakerKey, { - failureThreshold: PROVIDER_PROFILES.apikey.circuitBreakerThreshold, - resetTimeout: 60000, - }); - breaker.reset(); - - const log = mockLog(); - - // Send requests that all fail with 502 until the API-key profile threshold is reached. - for (let i = 0; i < PROVIDER_PROFILES.apikey.circuitBreakerThreshold; i++) { - await handleComboChat({ - body: {}, - combo, - handleSingleModel: mockHandler([502]), - isModelAvailable: () => true, - log, - settings: null, - allCombos: null, - }); - } - - // Breaker should now be OPEN - const status = breaker.getStatus(); - console.log("=== BREAKER STATUS AFTER THRESHOLD CALLS ===", status); - assert.equal( - status.state, - STATE.OPEN, - "Breaker should be OPEN after the API-key failure threshold is reached" - ); - assert.equal( - status.failureCount, - PROVIDER_PROFILES.apikey.circuitBreakerThreshold, - "Failure count should match the API-key profile threshold" - ); -}); - -test("handleComboChat: skips models with open circuit breaker", async () => { - // Set up: groq breaker is OPEN, fireworks breaker is CLOSED - const combo = { - name: "test-skip-combo", - models: [ - { model: "groq/llama-3.3-70b", weight: 0 }, - { model: "fireworks/deepseek-v3p1", weight: 0 }, - ], - strategy: "priority", - }; - const groqBreakerKey = getComboTargetBreakerKey(combo, 0); - const groqBreaker = getCircuitBreaker(groqBreakerKey, { - failureThreshold: 3, - resetTimeout: 60000, - }); - groqBreaker.reset(); - // Force open the breaker - groqBreaker._onFailure(); - groqBreaker._onFailure(); - groqBreaker._onFailure(); - assert.equal(groqBreaker.getStatus().state, STATE.OPEN); - - const fireworksBreakerKey = getComboTargetBreakerKey(combo, 1); - const fireworksBreaker = getCircuitBreaker(fireworksBreakerKey, { - failureThreshold: 5, - resetTimeout: 30000, - }); - fireworksBreaker.reset(); - - const log = mockLog(); - - const result = await handleComboChat({ - body: {}, - combo, - handleSingleModel: mockHandler([200]), // fireworks will succeed - isModelAvailable: () => true, - log, - settings: null, - allCombos: null, - }); - - assert.equal(result.ok, true, "Should succeed via fireworks fallback"); - - // Check logs show groq was skipped - const skipLog = log.entries.find( - (e) => e.msg.includes("circuit breaker OPEN") && e.msg.includes("groq") - ); - assert.ok(skipLog, "Should log that groq was skipped due to breaker"); -}); - -test("handleComboChat: returns 503 when all breakers are open", async () => { - const combo = { - name: "test-all-open", - models: [ - { model: "groq/llama-3.3-70b", weight: 0 }, - { model: "fireworks/deepseek-v3p1", weight: 0 }, - ], - strategy: "priority", - }; - - // Open both breakers explicitly before invoking the combo. - const groqBreaker = getCircuitBreaker(getComboTargetBreakerKey(combo, 0), { - failureThreshold: 3, - resetTimeout: 60000, - }); - groqBreaker.reset(); - groqBreaker._onFailure(); - groqBreaker._onFailure(); - groqBreaker._onFailure(); - assert.equal(groqBreaker.getStatus().state, STATE.OPEN); - - const fireworksBreaker = getCircuitBreaker(getComboTargetBreakerKey(combo, 1), { - failureThreshold: 5, - resetTimeout: 30000, - }); - fireworksBreaker.reset(); - for (let i = 0; i < 5; i++) fireworksBreaker._onFailure(); - assert.equal(fireworksBreaker.getStatus().state, STATE.OPEN); - - const log = mockLog(); - - const result = await handleComboChat({ - body: {}, - combo, - handleSingleModel: mockHandler([200]), // Won't be called - isModelAvailable: () => true, - log, - settings: null, - allCombos: null, - }); - - assert.equal(result.status, 503, "Should return 503"); - const body = await result.json(); - assert.ok(body.error.message.includes("circuit breakers open"), "Should mention breakers"); -}); - -test("handleComboChat: 429 errors also trigger circuit breaker", async () => { - const combo = { - name: "test-429", - models: [{ model: "cerebras/llama-3.3-70b", weight: 0 }], - strategy: "priority", - }; - const breakerKey = getComboTargetBreakerKey(combo); - const breaker = getCircuitBreaker(breakerKey, { - failureThreshold: 5, - resetTimeout: 30000, - }); - breaker.reset(); - - const log = mockLog(); - - // 5 x 429 should open breaker - for (let i = 0; i < 5; i++) { - await handleComboChat({ - body: {}, - combo, - handleSingleModel: mockHandler([429]), - isModelAvailable: () => true, - log, - settings: null, - allCombos: null, - }); - } - - assert.equal(breaker.getStatus().state, STATE.OPEN, "429s should open breaker"); -}); - -test("circuit breaker uses provider profile thresholds", () => { - // OAuth providers (e.g. claude) should have lower threshold - assert.equal(PROVIDER_PROFILES.oauth.circuitBreakerThreshold, 3); - // API providers (e.g. groq) should have higher threshold - assert.equal(PROVIDER_PROFILES.apikey.circuitBreakerThreshold, 5); -}); diff --git a/tests/unit/combo-config.test.ts b/tests/unit/combo-config.test.ts index 014ff4e44a..0c39a25993 100644 --- a/tests/unit/combo-config.test.ts +++ b/tests/unit/combo-config.test.ts @@ -13,7 +13,9 @@ test("getDefaultComboConfig returns a fresh copy of the defaults", () => { assert.notEqual(first, second); assert.equal(first.strategy, "priority"); assert.equal(first.maxRetries, 1); - assert.equal(first.timeoutMs, 600000); + assert.equal(first.retryDelayMs, 2000); + assert.ok(!("timeoutMs" in first)); + assert.ok(!("healthCheckEnabled" in first)); assert.equal(first.handoffThreshold, 0.85); assert.equal(first.maxMessagesForSummary, 30); assert.deepEqual(first.handoffProviders, ["codex"]); @@ -27,7 +29,6 @@ test("resolveComboConfig applies the full cascade from defaults to combo overrid { config: { maxRetries: 4, - timeoutMs: 45000, }, }, { @@ -47,12 +48,12 @@ test("resolveComboConfig applies the full cascade from defaults to combo overrid assert.equal(result.strategy, "round-robin"); assert.equal(result.retryDelayMs, 500); - assert.equal(result.timeoutMs, 45000); assert.equal(result.maxRetries, 4); - assert.equal(result.healthCheckEnabled, true); + assert.ok(!("timeoutMs" in result)); + assert.ok(!("healthCheckEnabled" in result)); }); -test("resolveComboConfig ignores null and undefined overrides", () => { +test("resolveComboConfig ignores null, undefined, and legacy resilience overrides", () => { const result = resolveComboConfig( { config: { @@ -75,7 +76,7 @@ test("resolveComboConfig ignores null and undefined overrides", () => { "openai" ); - assert.equal(result.timeoutMs, 600000); + assert.ok(!("timeoutMs" in result)); assert.equal(result.queueTimeoutMs, 15000); assert.equal(result.concurrencyPerModel, 9); assert.equal(result.trackMetrics, false); diff --git a/tests/unit/combo-context-relay.test.ts b/tests/unit/combo-context-relay.test.ts index 7675f05848..b3aa09f78d 100644 --- a/tests/unit/combo-context-relay.test.ts +++ b/tests/unit/combo-context-relay.test.ts @@ -13,12 +13,10 @@ const handoffDb = await import("../../src/lib/db/contextHandoffs.ts"); const { registerCodexConnection } = await import("../../open-sse/services/codexQuotaFetcher.ts"); const { clearSessions, touchSession } = await import("../../open-sse/services/sessionManager.ts"); const { resetAllComboMetrics } = await import("../../open-sse/services/comboMetrics.ts"); -const { resetAllCircuitBreakers, getCircuitBreaker } = - await import("../../src/shared/utils/circuitBreaker.ts"); +const { resetAllCircuitBreakers } = await import("../../src/shared/utils/circuitBreaker.ts"); const { resetAll: resetAllSemaphores } = await import("../../open-sse/services/rateLimitSemaphore.ts"); const { _resetAllDecks } = await import("../../src/shared/utils/shuffleDeck.ts"); -const { normalizeComboStep } = await import("../../src/lib/combos/steps.ts"); const originalFetch = globalThis.fetch; @@ -39,6 +37,24 @@ function okResponse(body = { choices: [{ message: { content: "ok" } }] }) { }); } +function providerBreakerOpenResponse() { + return new Response( + JSON.stringify({ + error: { + message: "Provider circuit breaker is open", + code: "provider_circuit_open", + }, + }), + { + status: 503, + headers: { + "content-type": "application/json", + "x-omniroute-provider-breaker": "open", + }, + } + ); +} + function buildQuotaResponse(usedPercent, resetAfterSeconds = 3600) { return new Response( JSON.stringify({ @@ -161,23 +177,13 @@ test("handleComboChat context-relay skips unavailable models and falls through t assert.deepEqual(calls, ["openai/gpt-4o-mini"]); }); -test("handleComboChat context-relay skips models with an open circuit breaker", async () => { +test("handleComboChat context-relay skips targets that report an open provider circuit breaker", async () => { const combo = { name: "relay-breaker", strategy: "context-relay", models: ["codex/gpt-5.4", "openai/gpt-4o-mini"], config: { maxRetries: 0 }, }; - const firstStep = normalizeComboStep(combo.models[0], { - comboName: combo.name, - index: 0, - }); - const breaker = getCircuitBreaker(`combo:${combo.name}:${firstStep.id}`, { - failureThreshold: 1, - resetTimeout: 60000, - }); - breaker._onFailure(); - const log = createLog(); const calls = []; @@ -188,6 +194,9 @@ test("handleComboChat context-relay skips models with an open circuit breaker", combo, handleSingleModel: async (_body, modelStr) => { calls.push(modelStr); + if (modelStr === "codex/gpt-5.4") { + return providerBreakerOpenResponse(); + } return okResponse(); }, isModelAvailable: async () => true, @@ -197,8 +206,10 @@ test("handleComboChat context-relay skips models with an open circuit breaker", }); assert.equal(result.ok, true); - assert.deepEqual(calls, ["openai/gpt-4o-mini"]); - assert.ok(log.entries.some((entry) => entry.msg.includes("circuit breaker OPEN"))); + assert.deepEqual(calls, ["codex/gpt-5.4", "openai/gpt-4o-mini"]); + assert.ok( + log.entries.some((entry) => String(entry.msg).includes("provider circuit breaker OPEN")) + ); }); test("handleComboChat context-relay persists a handoff when codex quota reaches the warning threshold", async () => { diff --git a/tests/unit/combo-routing-engine.test.ts b/tests/unit/combo-routing-engine.test.ts index c3e267277a..c8dd07e2ac 100644 --- a/tests/unit/combo-routing-engine.test.ts +++ b/tests/unit/combo-routing-engine.test.ts @@ -24,8 +24,7 @@ const { saveModelsDevCapabilities, clearModelsDevCapabilities } = await import("../../src/lib/modelsDevSync.ts"); const { getComboMetrics, recordComboRequest, resetAllComboMetrics } = await import("../../open-sse/services/comboMetrics.ts"); -const { getCircuitBreaker, resetAllCircuitBreakers } = - await import("../../src/shared/utils/circuitBreaker.ts"); +const { resetAllCircuitBreakers } = await import("../../src/shared/utils/circuitBreaker.ts"); const { acquire: acquireSemaphore, resetAll: resetAllSemaphores } = await import("../../open-sse/services/rateLimitSemaphore.ts"); const { _resetAllDecks } = await import("../../src/shared/utils/shuffleDeck.ts"); @@ -54,6 +53,24 @@ function errorResponse(status: number, message: string = `Error ${status}`) { }); } +function providerBreakerOpenResponse() { + return new Response( + JSON.stringify({ + error: { + message: "Provider circuit breaker is open", + code: "provider_circuit_open", + }, + }), + { + status: 503, + headers: { + "content-type": "application/json", + "x-omniroute-provider-breaker": "open", + }, + } + ); +} + function streamResponse(chunks: any[]) { return new Response(chunks.join(""), { status: 200, @@ -83,7 +100,7 @@ function capabilityEntry(limitContext: any) { }; } -function getComboTargetBreakerKey(comboName: string, index: number, stepInput: any) { +function getComboTargetExecutionKey(comboName: string, index: number, stepInput: any) { const step = normalizeComboStep(stepInput, { comboName, index }); if (!step) throw new Error(`Failed to normalize combo step for ${comboName}#${index}`); return `combo:${comboName}:${step.id}`; @@ -740,6 +757,52 @@ test("shouldFallbackComboBadRequest only flags known provider-scoped 400 pattern assert.equal(shouldFallbackComboBadRequest(429, "prohibited_content"), false); assert.equal(shouldFallbackComboBadRequest(400, null), false); assert.equal(shouldFallbackComboBadRequest(400, "generic bad request"), false); + // Chinese transient errors (ModelScope/Qwen) + assert.equal( + shouldFallbackComboBadRequest(400, "[400]: 抱歉,服务遇到了一点小状况,请您稍后重试。"), + true + ); + assert.equal(shouldFallbackComboBadRequest(400, "服务遇到了一点小状况"), true); + assert.equal(shouldFallbackComboBadRequest(400, "请稍后重试"), true); + // Model not supported errors + assert.equal( + shouldFallbackComboBadRequest( + 400, + "Model id : XiaomiMiMo/MiMo-V2-Flash , has no provider supported" + ), + true + ); + assert.equal(shouldFallbackComboBadRequest(400, "no provider supported"), true); + assert.equal(shouldFallbackComboBadRequest(400, "model not found"), true); + assert.equal(shouldFallbackComboBadRequest(400, "model not available"), true); + // Function calling format errors + assert.equal( + shouldFallbackComboBadRequest(400, "function.arguments parameter must be in JSON format"), + true + ); + assert.equal( + shouldFallbackComboBadRequest( + 400, + '[400]: <400> InternalError.Algo.InvalidParameter: The "function.arguments" parameter of the code model must be in JSON format.' + ), + true + ); + assert.equal(shouldFallbackComboBadRequest(400, "tool arguments invalid format"), true); + // Input length range errors + assert.equal( + shouldFallbackComboBadRequest(400, "Range of input length should be [1, 98304]"), + true + ); + assert.equal(shouldFallbackComboBadRequest(400, "input length should be"), true); + // Content moderation errors (should fallback to next model) + assert.equal( + shouldFallbackComboBadRequest(400, "抱歉,您的内容包含敏感内容,请检查后重试"), + true + ); + assert.equal(shouldFallbackComboBadRequest(400, "内容存在敏感信息,无法响应"), true); + assert.equal(shouldFallbackComboBadRequest(400, "无法响应该请求"), true); + // Generic "please check" should NOT match (was too broad before) + assert.equal(shouldFallbackComboBadRequest(400, "请检查您的参数"), false); }); test("handleComboChat accepts binary and Responses-style 200 bodies but falls through malformed success payloads", async () => { @@ -992,7 +1055,7 @@ test("handleComboChat round-robin returns 404 when no models are configured", as test("handleComboChat round-robin falls through semaphore timeouts and malformed success payloads", async () => { const release = await acquireSemaphore( - getComboTargetBreakerKey("rr-timeout-fallback", 0, "model-a"), + getComboTargetExecutionKey("rr-timeout-fallback", 0, "model-a"), { maxConcurrency: 1, timeoutMs: 100, @@ -1330,38 +1393,36 @@ test("handleComboChat returns a 503 when every model is unavailable before execu assert.equal(payload.error.code, "ALL_ACCOUNTS_INACTIVE"); }); -test("handleComboChat returns the circuit-breaker unavailable response when all breakers are open", async () => { - for (const [index, modelStr] of ["openai/model-a", "openai/model-b"].entries()) { - const breaker = getCircuitBreaker( - getComboTargetBreakerKey("all-breakers-open", index, modelStr), - { - failureThreshold: 1, - resetTimeout: 60000, - } - ); - breaker._onFailure(); - } - +test("handleComboChat falls through targets that return provider circuit breaker open responses", async () => { + const calls = []; + const log = createLog(); const result = await handleComboChat({ body: {}, combo: { - name: "all-breakers-open", + name: "provider-breaker-open", strategy: "priority", models: ["openai/model-a", "openai/model-b"], }, - handleSingleModel: async () => { - throw new Error("handleSingleModel should not run when all breakers are open"); + handleSingleModel: async (_body, modelStr) => { + calls.push(modelStr); + if (modelStr === "openai/model-a") { + return providerBreakerOpenResponse(); + } + return okResponse(); }, isModelAvailable: async () => true, - log: createLog(), + log, settings: null, relayOptions: null as any, allCombos: null, relayOptions: null, }); - assert.equal(result.status, 503); - assert.match((await result.json()).error.message, /circuit breakers open/); + assert.equal(result.ok, true); + assert.deepEqual(calls, ["openai/model-a", "openai/model-b"]); + assert.ok( + log.entries.some((entry) => String(entry.msg).includes("provider circuit breaker OPEN")) + ); }); test("handleComboChat auto strategy honors LKGP after filtering to tool-capable models", async () => { @@ -1753,39 +1814,37 @@ test("handleComboChat round-robin resolves nested combos and returns inactive wh assert.equal(payload.error.code, "ALL_ACCOUNTS_INACTIVE"); }); -test("handleComboChat round-robin returns circuit-breaker unavailable when every model is open", async () => { - for (const [index, modelStr] of ["openai/model-a", "openai/model-b"].entries()) { - const breaker = getCircuitBreaker( - getComboTargetBreakerKey("rr-breakers-open", index, modelStr), - { - failureThreshold: 1, - resetTimeout: 60000, - } - ); - breaker._onFailure(); - } - +test("handleComboChat round-robin skips targets that return provider circuit breaker open responses", async () => { + const calls = []; + const log = createLog(); const result = await handleComboChat({ body: {}, combo: { - name: "rr-breakers-open", + name: "rr-provider-breaker-open", strategy: "round-robin", models: ["openai/model-a", "openai/model-b"], config: { maxRetries: 0 }, }, - handleSingleModel: async () => { - throw new Error("round-robin should not execute when all breakers are open"); + handleSingleModel: async (_body, modelStr) => { + calls.push(modelStr); + if (modelStr === "openai/model-a") { + return providerBreakerOpenResponse(); + } + return okResponse(); }, isModelAvailable: async () => true, - log: createLog(), + log, settings: null, relayOptions: null as any, allCombos: null, relayOptions: null, }); - assert.equal(result.status, 503); - assert.match((await result.json()).error.message, /circuit breakers open/); + assert.equal(result.ok, true); + assert.deepEqual(calls, ["openai/model-a", "openai/model-b"]); + assert.ok( + log.entries.some((entry) => String(entry.msg).includes("provider circuit breaker OPEN")) + ); }); test("handleComboChat round-robin retries a transient failure on the same model before succeeding", async () => { diff --git a/tests/unit/domain-branch-hardening.test.ts b/tests/unit/domain-branch-hardening.test.ts index 63d8e387d7..8f6071dbee 100644 --- a/tests/unit/domain-branch-hardening.test.ts +++ b/tests/unit/domain-branch-hardening.test.ts @@ -11,7 +11,6 @@ const core = await import("../../src/lib/db/core.ts"); const costRules = await import("../../src/domain/costRules.ts"); const fallbackPolicy = await import("../../src/domain/fallbackPolicy.ts"); const lockoutPolicy = await import("../../src/domain/lockoutPolicy.ts"); -const modelAvailability = await import("../../src/domain/modelAvailability.ts"); const providerExpiration = await import("../../src/domain/providerExpiration.ts"); const quotaCache = await import("../../src/domain/quotaCache.ts"); const comboResolver = await import("../../src/domain/comboResolver.ts"); @@ -28,7 +27,6 @@ function isoFromNow(offsetMs) { async function resetStorage() { costRules.resetCostData(); fallbackPolicy.resetAllFallbacks(); - modelAvailability.resetAllAvailability(); providerExpiration.resetExpirations(); quotaCache.stopBackgroundRefresh(); core.resetDbInstance(); @@ -63,7 +61,6 @@ test.after(async () => { quotaCache.stopBackgroundRefresh(); costRules.resetCostData(); fallbackPolicy.resetAllFallbacks(); - modelAvailability.resetAllAvailability(); providerExpiration.resetExpirations(); core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); @@ -192,30 +189,6 @@ test("resolveComboModel also covers implicit defaults and missing optional field assert.deepEqual(comboResolver.getComboFallbacks({}, 0), []); }); -test("modelAvailability tracks missing, active and expired cooldowns", () => { - let now = 1_000; - Date.now = () => now; - - assert.equal(modelAvailability.isModelAvailable("openai", "gpt-4o"), true); - - modelAvailability.setModelUnavailable("openai", "gpt-4o", 100, undefined); - modelAvailability.setModelUnavailable("anthropic", "claude-sonnet", 500, "capacity"); - - assert.equal(modelAvailability.isModelAvailable("openai", "gpt-4o"), false); - assert.equal(modelAvailability.getUnavailableCount(), 2); - - const report = modelAvailability.getAvailabilityReport(); - assert.equal(report.length, 2); - assert.equal(report[0].reason, "unknown"); - assert.equal(report[1].reason, "capacity"); - - now += 150; - assert.equal(modelAvailability.isModelAvailable("openai", "gpt-4o"), true); - assert.equal(modelAvailability.clearModelUnavailability("openai", "gpt-4o"), false); - assert.equal(modelAvailability.clearModelUnavailability("anthropic", "claude-sonnet"), true); - assert.equal(modelAvailability.getUnavailableCount(), 0); -}); - test("providerExpiration derives status, sorting, summary and header-based expiration hints", () => { const expired = providerExpiration.setExpiration( "conn-expired", diff --git a/tests/unit/error-classification.test.ts b/tests/unit/error-classification.test.ts index 9fb24d7181..0f6be74c08 100644 --- a/tests/unit/error-classification.test.ts +++ b/tests/unit/error-classification.test.ts @@ -6,7 +6,7 @@ const { checkFallbackError, getProviderProfile, parseRetryFromErrorText } = const { getProviderCategory } = await import("../../open-sse/config/providerRegistry.ts"); -const { COOLDOWN_MS, PROVIDER_PROFILES, RateLimitReason } = +const { BACKOFF_CONFIG, COOLDOWN_MS, PROVIDER_PROFILES, RateLimitReason } = await import("../../open-sse/config/constants.ts"); // ─── Provider Category Tests ──────────────────────────────────────────────── @@ -41,12 +41,36 @@ test("getProviderCategory: unknown provider defaults to 'apikey'", () => { test("getProviderProfile: OAuth provider returns oauth profile", () => { const profile = getProviderProfile("claude"); - assert.deepEqual(profile, PROVIDER_PROFILES.oauth); + assert.equal(profile.baseCooldownMs, PROVIDER_PROFILES.oauth.transientCooldown); + assert.equal(profile.useUpstreamRetryHints, false); + assert.equal(profile.maxBackoffSteps, PROVIDER_PROFILES.oauth.maxBackoffLevel); + assert.equal(profile.failureThreshold, PROVIDER_PROFILES.oauth.circuitBreakerThreshold); + assert.equal(profile.resetTimeoutMs, PROVIDER_PROFILES.oauth.circuitBreakerReset); + assert.equal(profile.transientCooldown, PROVIDER_PROFILES.oauth.transientCooldown); + assert.equal( + profile.rateLimitCooldown, + profile.useUpstreamRetryHints ? 0 : profile.baseCooldownMs + ); + assert.equal(profile.maxBackoffLevel, PROVIDER_PROFILES.oauth.maxBackoffLevel); + assert.equal(profile.circuitBreakerThreshold, PROVIDER_PROFILES.oauth.circuitBreakerThreshold); + assert.equal(profile.circuitBreakerReset, PROVIDER_PROFILES.oauth.circuitBreakerReset); }); test("getProviderProfile: API provider returns apikey profile", () => { const profile = getProviderProfile("groq"); - assert.deepEqual(profile, PROVIDER_PROFILES.apikey); + assert.equal(profile.baseCooldownMs, PROVIDER_PROFILES.apikey.transientCooldown); + assert.equal(profile.useUpstreamRetryHints, true); + assert.equal(profile.maxBackoffSteps, PROVIDER_PROFILES.apikey.maxBackoffLevel); + assert.equal(profile.failureThreshold, PROVIDER_PROFILES.apikey.circuitBreakerThreshold); + assert.equal(profile.resetTimeoutMs, PROVIDER_PROFILES.apikey.circuitBreakerReset); + assert.equal(profile.transientCooldown, PROVIDER_PROFILES.apikey.transientCooldown); + assert.equal( + profile.rateLimitCooldown, + profile.useUpstreamRetryHints ? 0 : profile.baseCooldownMs + ); + assert.equal(profile.maxBackoffLevel, PROVIDER_PROFILES.apikey.maxBackoffLevel); + assert.equal(profile.circuitBreakerThreshold, PROVIDER_PROFILES.apikey.circuitBreakerThreshold); + assert.equal(profile.circuitBreakerReset, PROVIDER_PROFILES.apikey.circuitBreakerReset); }); test("getProviderProfile: profiles have different thresholds", () => { @@ -64,7 +88,7 @@ test("getProviderProfile: profiles have different thresholds", () => { // ─── Exponential Backoff for Transient Errors ─────────────────────────────── -test("502 transient: exponential backoff 5s → 10s → 20s → 40s → 60s (capped)", () => { +test("502 transient: exponential backoff doubles until the configured max backoff step", () => { const cooldowns = []; for (let level = 0; level < 6; level++) { const result = checkFallbackError(502, "", level, null, null); @@ -73,14 +97,14 @@ test("502 transient: exponential backoff 5s → 10s → 20s → 40s → 60s (cap assert.equal(result.newBackoffLevel, level + 1); assert.equal(result.reason, RateLimitReason.SERVER_ERROR); } - // Without provider: uses COOLDOWN_MS.transientInitial (5s) as base - assert.equal(cooldowns[0], COOLDOWN_MS.transientInitial); // 5s - assert.equal(cooldowns[1], COOLDOWN_MS.transientInitial * 2); // 10s - assert.equal(cooldowns[2], COOLDOWN_MS.transientInitial * 4); // 20s - assert.equal(cooldowns[3], COOLDOWN_MS.transientInitial * 8); // 40s - // Level 4: 5s * 16 = 80s → capped at 60s - assert.equal(cooldowns[4], COOLDOWN_MS.transientMax); // 60s - assert.equal(cooldowns[5], COOLDOWN_MS.transientMax); // 60s (stays capped) + assert.deepEqual(cooldowns, [ + COOLDOWN_MS.transientInitial, + COOLDOWN_MS.transientInitial * 2, + COOLDOWN_MS.transientInitial * 4, + COOLDOWN_MS.transientInitial * 8, + COOLDOWN_MS.transientInitial * 16, + COOLDOWN_MS.transientInitial * 32, + ]); }); test("502 with OAuth provider: uses oauth profile transientCooldown", () => { @@ -116,11 +140,13 @@ test("429 rate limit: still uses quota-based exponential backoff", () => { assert.equal(result.reason, RateLimitReason.RATE_LIMIT_EXCEEDED); }); -test("401 auth error: still uses flat cooldown, no backoff", () => { +test("401 auth error: returns terminal auth semantics without connection cooldown", () => { const result = checkFallbackError(401, "", 0, null, "groq"); assert.equal(result.shouldFallback, true); - assert.equal(result.cooldownMs, COOLDOWN_MS.unauthorized); + assert.equal(result.cooldownMs, 0); + assert.equal(result.baseCooldownMs, 0); assert.equal(result.newBackoffLevel, undefined); + assert.equal(result.reason, RateLimitReason.AUTH_ERROR); }); test("400 bad request: still returns shouldFallback false", () => { @@ -162,7 +188,7 @@ test("parseRetryFromErrorText: parses will reset after variant", () => { // ─── T06: Keyword Matching for Long Cooldowns ──────────────────────────────── -test("quota will reset keyword triggers long cooldown from body", () => { +test("quota reset text is ignored when upstream retry hints are disabled", () => { const result = checkFallbackError( 429, "Your quota will reset after 27h41m36s", @@ -172,19 +198,31 @@ test("quota will reset keyword triggers long cooldown from body", () => { null ); assert.equal(result.shouldFallback, true); - assert.ok(result.cooldownMs > 60_000, "cooldownMs should be > 60s"); - assert.equal(result.newBackoffLevel, 0, "backoffLevel should reset to 0"); + assert.equal(result.cooldownMs, PROVIDER_PROFILES.oauth.transientCooldown); + assert.equal(result.newBackoffLevel, 1); + assert.equal(result.usedUpstreamRetryHint, false); }); -test("exhausted your capacity keyword triggers long cooldown", () => { +test("quota reset text is honored when upstream retry hints are enabled", () => { const result = checkFallbackError( 429, "You have exhausted your capacity. Your quota will reset after 2h", 0, null, - "antigravity", + "groq", null ); assert.equal(result.shouldFallback, true); - assert.ok(result.cooldownMs > 60_000); + assert.equal(result.cooldownMs, 2 * 60 * 60 * 1000); + assert.equal(result.newBackoffLevel, 0); + assert.equal(result.usedUpstreamRetryHint, true); +}); + +test("high transient backoff levels clamp to the configured maxBackoffSteps", () => { + const result = checkFallbackError(502, "", BACKOFF_CONFIG.maxLevel + 5, null, null); + assert.equal(result.newBackoffLevel, BACKOFF_CONFIG.maxLevel); + assert.equal( + result.cooldownMs, + COOLDOWN_MS.transientInitial * Math.pow(2, BACKOFF_CONFIG.maxLevel) + ); }); diff --git a/tests/unit/error-classifier.test.ts b/tests/unit/error-classifier.test.ts index 13184d076c..846f89fb87 100644 --- a/tests/unit/error-classifier.test.ts +++ b/tests/unit/error-classifier.test.ts @@ -65,3 +65,21 @@ test("classifyProviderError: 403 with project string as plain string body => PRO const result = classifyProviderError(403, body); assert.equal(result, PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR); }); + +test("classifyProviderError: 429 with daily quota signal => QUOTA_EXHAUSTED", () => { + const body = JSON.stringify({ + error: { + message: + "You have exceeded today's quota for model moonshotai/Kimi-K2.5, please try again tomorrow", + }, + }); + const result = classifyProviderError(429, body); + assert.equal(result, PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED); +}); + +test("classifyProviderError: 429 with 'daily quota' signal => QUOTA_EXHAUSTED", () => { + const result = classifyProviderError(429, { + error: { message: "You have reached your daily quota limit" }, + }); + assert.equal(result, PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED); +}); diff --git a/tests/unit/rate-limit-manager.test.ts b/tests/unit/rate-limit-manager.test.ts index cb1aa96b6a..e61d523103 100644 --- a/tests/unit/rate-limit-manager.test.ts +++ b/tests/unit/rate-limit-manager.test.ts @@ -157,9 +157,14 @@ test("rate limit manager parses retry hints from response bodies and locks model "gpt-4o" ); - const lockout = accountFallback.getModelLockoutInfo("openai", "conn-body", "gpt-4o"); - assert.equal(lockout.reason, "rate_limit_exceeded"); - assert.ok(lockout.remainingMs > 0); + assert.equal(accountFallback.getModelLockoutInfo("openai", "conn-body", "gpt-4o"), null); + const limiterState = await rateLimitManager.__getLimiterStateForTests( + "openai", + "conn-body", + "gpt-4o" + ); + assert.equal(limiterState?.key, "openai:conn-body"); + assert.equal(rateLimitManager.getRateLimitStatus("openai", "conn-body").active, true); rateLimitManager.updateFromResponseBody( "openai", diff --git a/tests/unit/sse-auth.test.ts b/tests/unit/sse-auth.test.ts index 98b31f54c8..04d558a7c2 100644 --- a/tests/unit/sse-auth.test.ts +++ b/tests/unit/sse-auth.test.ts @@ -14,6 +14,7 @@ const settingsDb = await import("../../src/lib/db/settings.ts"); const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); const auth = await import("../../src/sse/services/auth.ts"); const quotaCache = await import("../../src/domain/quotaCache.ts"); +const { COOLDOWN_MS } = await import("../../open-sse/config/constants.ts"); async function resetStorage() { core.resetDbInstance(); @@ -760,7 +761,7 @@ test("getProviderCredentials exposes copilotToken when present in providerSpecif assert.equal(selected.copilotToken, "copilot-token-value"); }); -test("markAccountUnavailable uses configured cooldowns for local 404 model lockouts", async () => { +test("markAccountUnavailable keeps local 404 failures model-scoped with the local not-found cooldown", async () => { await settingsDb.updateSettings({ providerProfiles: { apikey: { @@ -790,7 +791,7 @@ test("markAccountUnavailable uses configured cooldowns for local 404 model locko const updated = await providersDb.getProviderConnectionById(connection.id); assert.equal(result.shouldFallback, true); - assert.equal(result.cooldownMs, 250); + assert.equal(result.cooldownMs, COOLDOWN_MS.notFoundLocal); assert.equal(updated.testStatus, "active"); assert.equal(updated.rateLimitedUntil, undefined); assert.equal(updated.lastErrorType, "not_found"); @@ -843,7 +844,7 @@ test("markAccountUnavailable applies a model-only lockout for compatible provide assert.equal(Number(updated.errorCode), 429); }); -test("markAccountUnavailable honors configured api-key rate-limit cooldowns", async () => { +test("markAccountUnavailable uses the unified configured api-key connection cooldown", async () => { await settingsDb.updateSettings({ providerProfiles: { apikey: { @@ -869,7 +870,7 @@ test("markAccountUnavailable honors configured api-key rate-limit cooldowns", as ); assert.equal(result.shouldFallback, true); - assert.equal(result.cooldownMs, 125); + assert.equal(result.cooldownMs, 200); }); test("markAccountUnavailable stores Codex scope-specific cooldowns without a global rate limit", async () => { @@ -1021,7 +1022,7 @@ test("markAccountUnavailable auto-disables permanently banned accounts when the assert.equal(result.shouldFallback, true); assert.equal(updated.isActive, false); - assert.equal(updated.testStatus, "unavailable"); + assert.equal(updated.testStatus, "banned"); }); test("markAccountUnavailable leaves permanently banned accounts active when auto-disable is disabled", async () => { @@ -1041,7 +1042,7 @@ test("markAccountUnavailable leaves permanently banned accounts active when auto assert.equal(result.shouldFallback, true); assert.equal(updated.isActive, true); - assert.equal(updated.testStatus, "unavailable"); + assert.equal(updated.testStatus, "banned"); }); test("markAccountUnavailable swallows auto-disable persistence errors", async () => { @@ -1085,7 +1086,7 @@ test("markAccountUnavailable swallows auto-disable persistence errors", async () assert.equal(result.shouldFallback, true); assert.equal(updated.isActive, true); - assert.equal(updated.testStatus, "unavailable"); + assert.equal(updated.testStatus, "banned"); } finally { db.prepare = originalPrepare; } diff --git a/tests/unit/thundering-herd.test.ts b/tests/unit/thundering-herd.test.ts index aa1c28b5c2..e3af4f81f1 100644 --- a/tests/unit/thundering-herd.test.ts +++ b/tests/unit/thundering-herd.test.ts @@ -9,7 +9,7 @@ import assert from "node:assert/strict"; const { checkFallbackError, getProviderProfile } = await import("../../open-sse/services/accountFallback.ts"); -const { PROVIDER_PROFILES, DEFAULT_API_LIMITS, COOLDOWN_MS, RateLimitReason } = +const { BACKOFF_CONFIG, PROVIDER_PROFILES, DEFAULT_API_LIMITS, COOLDOWN_MS, RateLimitReason } = await import("../../open-sse/config/constants.ts"); const { getProviderCategory } = await import("../../open-sse/config/providerRegistry.ts"); @@ -36,10 +36,13 @@ test("API profile has shorter transient cooldown", () => { // ─── Backoff Ceiling Tests (prevents infinite growth) ─────────────────────── -test("Exponential backoff is capped at transientMax for high backoff levels", () => { - // Level 20 → 5s * 2^20 = 5.2M ms, but capped at 60s +test("Exponential backoff clamps to the configured maxBackoffLevel", () => { const result = checkFallbackError(502, "", 20, null, null); - assert.equal(result.cooldownMs, COOLDOWN_MS.transientMax); + assert.equal(result.newBackoffLevel, BACKOFF_CONFIG.maxLevel); + assert.equal( + result.cooldownMs, + COOLDOWN_MS.transientInitial * Math.pow(2, BACKOFF_CONFIG.maxLevel) + ); }); test("API provider backoff level caps at profile maxBackoffLevel", () => { diff --git a/tests/unit/translator-helper-branches.test.ts b/tests/unit/translator-helper-branches.test.ts index e31c393352..33a59a0302 100644 --- a/tests/unit/translator-helper-branches.test.ts +++ b/tests/unit/translator-helper-branches.test.ts @@ -306,9 +306,10 @@ test("claudeHelper validates content, ordering and request preparation branches" assert.equal(prepared.messages[4].content[0].type, "tool_result"); assert.deepEqual( prepared.messages[5].content.map((block) => block.type), - ["thinking", "text"] + ["redacted_thinking", "text"] ); assert.ok(prepared.messages[5].content[0].signature); + assert.equal(prepared.messages[5].content[0].thinking, undefined); assert.equal(prepared.tools.length, 2); assert.equal(prepared.tools[0].cache_control, undefined); assert.deepEqual(prepared.tools[1].cache_control, { type: "ephemeral", ttl: "1h" });