Repository navigation
feat: v0.2.0 — advanced routing services, cost analytics, pricing overhaul - #20
Conversation
…ricing overhaul Added: - 8 new open-sse services (account selector, IP filter, session manager, etc.) - 6 new dashboard settings tabs (IP filter, system prompt, thinking budget, pricing) - Usage cost dashboard with provider cost donut, cost trend line, model cost column - Pricing API merging registry + custom + pricing-only models - 9 unit tests for all new services Changed: - Usage analytics layout redesigned with prominent cost display - DailyTrendChart upgraded to ComposedChart with dual Y-axes Fixed: - Pricing page now shows custom/imported models - Icon rendering (material-symbols-rounded → outlined)
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
Summary of ChangesHello @diegosouzapw, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This release introduces a suite of advanced routing and management services, significantly enhancing the platform's capabilities for intelligent request handling, cost optimization, and security. The dashboard has been expanded with new configuration options and detailed analytics, providing users with greater control and visibility over their AI interactions and expenditures. These changes collectively aim to improve efficiency, flexibility, and transparency in managing diverse AI models and providers. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a massive suite of advanced features, including sophisticated routing services, cost analytics, and security hardening, along with 8 new services in open-sse and an overhaul of the pricing and routing UI. While the changes are extensive, well-structured, and include comprehensive unit tests, a security audit identified several critical and high-severity vulnerabilities. These primarily relate to missing authentication on new API routes, insecure handling of client IP addresses, and the ability for settings APIs to allow unauthenticated modification of core proxy behavior. The IP filtering logic is also susceptible to spoofing and contains a bug in CIDR matching for broad masks. Remediation of these security issues is essential before deployment, and there is a minor suggestion regarding documentation.
| export async function PUT(request) { | ||
| try { | ||
| const body = await request.json(); | ||
|
|
||
| // Configure entire filter | ||
| if (body.enabled !== undefined || body.mode || body.blacklist || body.whitelist) { | ||
| configureIPFilter(body); | ||
| } | ||
|
|
||
| // Add/remove individual IPs | ||
| if (body.addBlacklist) addToBlacklist(body.addBlacklist); | ||
| if (body.removeBlacklist) removeFromBlacklist(body.removeBlacklist); | ||
| if (body.addWhitelist) addToWhitelist(body.addWhitelist); | ||
| if (body.removeWhitelist) removeFromWhitelist(body.removeWhitelist); | ||
|
|
||
| // Temp bans | ||
| if (body.tempBan) { | ||
| tempBanIP(body.tempBan.ip, body.tempBan.durationMs || 3600000, body.tempBan.reason || "Manual ban"); | ||
| } | ||
| if (body.removeBan) removeTempBan(body.removeBan); | ||
|
|
||
| return NextResponse.json(getIPFilterConfig()); | ||
| } catch (error) { | ||
| return NextResponse.json({ error: error.message }, { status: 500 }); | ||
| } | ||
| } |
There was a problem hiding this comment.
The API routes for managing sensitive system settings, such as IP filters, lack any authentication or authorization checks. Although the dashboard includes a 'Require login' setting, it is not enforced in these API handlers. An unauthenticated attacker could modify the proxy's configuration, potentially leading to unauthorized access or denial of service. This issue also applies to other settings routes like system-prompt and thinking-budget.
| function extractClientIP(req) { | ||
| return ( | ||
| req.headers?.["x-forwarded-for"]?.split(",")[0].trim() || | ||
| req.headers?.["x-real-ip"] || | ||
| req.headers?.["cf-connecting-ip"] || | ||
| req.socket?.remoteAddress || | ||
| req.ip || | ||
| "unknown" | ||
| ); | ||
| } |
There was a problem hiding this comment.
The extractClientIP function trusts client-supplied headers like X-Forwarded-For to determine the client's IP address. If the application is not deployed behind a trusted proxy that sanitizes these headers, an attacker can easily bypass the IP blacklist or spoof a whitelisted IP by providing a fake header. This undermines the security of the IP-based access control system.
| const rangeNum = ipToNum(range); | ||
| if (ipNum === null || rangeNum === null) return false; | ||
| const maskBits = (-1 << (32 - mask)) >>> 0; | ||
| return (ipNum & maskBits) === (rangeNum & maskBits); |
There was a problem hiding this comment.
The matchesCIDR function contains a logic error when handling a mask of 0 (e.g., 0.0.0.0/0). In JavaScript, bitwise shift amounts are masked to 5 bits, so x << 32 is equivalent to x << 0. This results in a maskBits value of 0xFFFFFFFF for a /0 mask, causing it to match only the base IP instead of the entire internet. This could lead to security bypasses if /0 is used in a blacklist or unexpected blocks if used in a whitelist.
| return (ipNum & maskBits) === (rangeNum & maskBits); | |
| const maskBits = mask === 0 ? 0 : (-1 << (32 - mask)) >>> 0; |
| export function wildcardMatch(model, pattern) { | ||
| if (!model || !pattern) return false; | ||
| if (pattern === "*") return true; | ||
| if (pattern === model) return true; | ||
|
|
||
| // Convert glob pattern to regex | ||
| const regexStr = pattern | ||
| .replace(/[.+^${}()|[\]\\]/g, "\\$&") | ||
| .replace(/\*/g, ".*") | ||
| .replace(/\?/g, "."); | ||
| const regex = new RegExp(`^${regexStr}$`, "i"); | ||
| return regex.test(model); | ||
| } |
There was a problem hiding this comment.
The wildcardMatch function constructs a regular expression from a user-supplied pattern by replacing * with .*. If a pattern contains multiple wildcards (e.g., *a*b*c*), it can lead to catastrophic backtracking when matched against certain inputs, causing a Regular Expression Denial of Service (ReDoS). Since these patterns can be configured via the settings API, this poses a risk to the availability of the proxy.
| @@ -16,20 +225,21 @@ export function getQuotaCooldown(backoffLevel = 0) { | |||
| * @param {number} status - HTTP status code | |||
| * @param {string} errorText - Error message text | |||
| * @param {number} backoffLevel - Current backoff level for exponential backoff | |||
| * @returns {{ shouldFallback: boolean, cooldownMs: number, newBackoffLevel?: number }} | |||
| * @param {string} [model] - Optional model name for model-level lockout | |||
| * @returns {{ shouldFallback: boolean, cooldownMs: number, newBackoffLevel?: number, reason?: string }} | |||
| */ | |||
| export function checkFallbackError(status, errorText, backoffLevel = 0) { | |||
| export function checkFallbackError(status, errorText, backoffLevel = 0, model = null) { | |||
| // Check error message FIRST - specific patterns take priority over status codes | |||
| if (errorText) { | |||
| const errorStr = typeof errorText === "string" ? errorText : JSON.stringify(errorText); | |||
| const lowerError = errorStr.toLowerCase(); | |||
|
|
|||
| if (lowerError.includes("no credentials")) { | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.notFound }; | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.notFound, reason: RateLimitReason.AUTH_ERROR }; | |||
| } | |||
|
|
|||
| if (lowerError.includes("request not allowed")) { | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.requestNotAllowed }; | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.requestNotAllowed, reason: RateLimitReason.RATE_LIMIT_EXCEEDED }; | |||
| } | |||
|
|
|||
| // Rate limit keywords - exponential backoff | |||
| @@ -41,24 +251,26 @@ export function checkFallbackError(status, errorText, backoffLevel = 0) { | |||
| lowerError.includes("overloaded") | |||
| ) { | |||
| const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel); | |||
| const reason = classifyErrorText(errorStr); | |||
| return { | |||
| shouldFallback: true, | |||
| cooldownMs: getQuotaCooldown(backoffLevel), | |||
| newBackoffLevel: newLevel, | |||
| reason, | |||
| }; | |||
| } | |||
| } | |||
|
|
|||
| if (status === HTTP_STATUS.UNAUTHORIZED) { | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.unauthorized }; | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.unauthorized, reason: RateLimitReason.AUTH_ERROR }; | |||
| } | |||
|
|
|||
| if (status === HTTP_STATUS.PAYMENT_REQUIRED || status === HTTP_STATUS.FORBIDDEN) { | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.paymentRequired }; | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.paymentRequired, reason: RateLimitReason.QUOTA_EXHAUSTED }; | |||
| } | |||
|
|
|||
| if (status === HTTP_STATUS.NOT_FOUND) { | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.notFound }; | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.notFound, reason: RateLimitReason.UNKNOWN }; | |||
| } | |||
|
|
|||
| // 429 - Rate limit with exponential backoff | |||
| @@ -68,10 +280,11 @@ export function checkFallbackError(status, errorText, backoffLevel = 0) { | |||
| shouldFallback: true, | |||
| cooldownMs: getQuotaCooldown(backoffLevel), | |||
| newBackoffLevel: newLevel, | |||
| reason: RateLimitReason.RATE_LIMIT_EXCEEDED, | |||
| }; | |||
| } | |||
|
|
|||
| // Transient errors | |||
| // Transient / server errors — DON'T increase backoff level | |||
| const transientStatuses = [ | |||
| HTTP_STATUS.NOT_ACCEPTABLE, | |||
| HTTP_STATUS.REQUEST_TIMEOUT, | |||
| @@ -81,18 +294,20 @@ export function checkFallbackError(status, errorText, backoffLevel = 0) { | |||
| HTTP_STATUS.GATEWAY_TIMEOUT, | |||
| ]; | |||
| if (transientStatuses.includes(status)) { | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.transient }; | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.transient, reason: RateLimitReason.SERVER_ERROR }; | |||
| } | |||
|
|
|||
| // 400 Bad Request - don't fallback (same request will fail on all accounts) | |||
| if (status === HTTP_STATUS.BAD_REQUEST) { | |||
| return { shouldFallback: false, cooldownMs: 0 }; | |||
| return { shouldFallback: false, cooldownMs: 0, reason: RateLimitReason.UNKNOWN }; | |||
| } | |||
|
|
|||
| // All other errors - fallback with transient cooldown | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.transient }; | |||
| return { shouldFallback: true, cooldownMs: COOLDOWN_MS.transient, reason: RateLimitReason.UNKNOWN }; | |||
| } | |||
|
|
|||
| // ─── Account State Management ─────────────────────────────────────────────── | |||
|
|
|||
| /** | |||
| * Check if account is currently unavailable (cooldown not expired) | |||
| */ | |||
| @@ -110,8 +325,6 @@ export function getUnavailableUntil(cooldownMs) { | |||
|
|
|||
| /** | |||
| * Get the earliest rateLimitedUntil from a list of accounts | |||
| * @param {Array} accounts - Array of account objects with rateLimitedUntil | |||
| * @returns {string|null} Earliest rateLimitedUntil ISO string, or null | |||
| */ | |||
There was a problem hiding this comment.
The JSDoc comments for several utility functions in this file (getEarliestRateLimitedUntil, formatRetryAfter, resetAccountState, and applyErrorState) have been removed. While the function logic remains, removing the documentation makes the code harder to understand and maintain, especially for new contributors. Please consider restoring these JSDoc comments, including @param and @returns tags, to clarify the function signatures and their purpose.
There was a problem hiding this comment.
Pull request overview
This is a major feature release (v0.2.0) that adds advanced routing services, comprehensive cost analytics, and a completely redesigned pricing management system to OmniRoute.
Changes:
- 8 new open-sse services for intelligent routing, session management, IP filtering, context compression, thinking budget control, system prompt injection, signature caching, and wildcard routing
- 6 new dashboard settings tabs and usage monitoring components
- Enhanced usage analytics with provider cost donut chart, cost trend overlay, and per-model cost tracking
- Overhauled pricing API that merges three sources: registry models, custom models, and pricing-only models
Reviewed changes
Copilot reviewed 47 out of 50 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Version bump 0.1.0 → 0.2.0 |
| tsconfig.json | Added antigravity-manager-analysis to exclude list |
| next.config.mjs | Added empty turbopack config |
| open-sse/services/wildcardRouter.js | New: glob-style model routing with specificity ranking |
| open-sse/services/thinkingBudget.js | New: token budget management for reasoning models (4 modes) |
| open-sse/services/systemPrompt.js | New: global system prompt injection for all requests |
| open-sse/services/signatureCache.js | New: 3-layer dynamic cache for thinking signatures |
| open-sse/services/sessionManager.js | New: session fingerprinting and tracking |
| open-sse/services/ipFilter.js | New: IP allowlist/blocklist with CIDR and wildcard support |
| open-sse/services/contextManager.js | New: 3-layer context compression to prevent prompt length errors |
| open-sse/services/accountSelector.js | New: P2C account selection with health scoring |
| open-sse/services/accountFallback.js | Enhanced rate limiting with per-model lockouts and error classification |
| open-sse/services/rateLimitManager.js | Added response body parsing for retry-after info |
| open-sse/config/constants.js | Added RateLimitReason enum and BACKOFF_STEPS_MS |
| open-sse/translator/index.js | Integrated thinking budget control into translation pipeline |
| src/sse/services/auth.js | Updated markAccountUnavailable signature to include model parameter |
| src/app/api/settings/thinking-budget/route.js | New: thinking budget configuration API |
| src/app/api/settings/system-prompt/route.js | New: system prompt configuration API |
| src/app/api/settings/ip-filter/route.js | New: IP filter configuration API |
| src/app/api/sessions/route.js | New: active sessions API |
| src/app/api/rate-limits/route.js | New: rate limit status API |
| src/app/api/pricing/models/route.js | New: merged model catalog API (registry + custom + pricing) |
| src/app/(dashboard)/dashboard/settings/page.js | Added AI and Pricing tabs |
| src/app/(dashboard)/dashboard/settings/components/* | 4 new settings components (ThinkingBudget, SystemPrompt, IPFilter, Pricing) |
| src/app/(dashboard)/dashboard/usage/page.js | Added Sessions tab and enhanced Limits tab |
| src/app/(dashboard)/dashboard/usage/components/* | 2 new usage components (SessionsTab, RateLimitStatus) |
| src/shared/components/analytics/charts.js | Enhanced with cost tracking: provider donut, cost trend line, model cost column |
| src/shared/components/UsageAnalytics.js | Reorganized layout with cost-focused analytics |
| tests/unit/*.test.mjs | 9 comprehensive unit test files for all new services |
| docs/* | Updated architecture and codebase documentation |
| CHANGELOG.md | Comprehensive v0.2.0 release notes |
| README.md | Updated feature list with new capabilities |
| .gitignore | Added cloud/ directory exclusion |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export async function GET() { | ||
| try { | ||
| return NextResponse.json(getSystemPromptConfig()); | ||
| } catch (error) { | ||
| return NextResponse.json({ error: error.message }, { status: 500 }); | ||
| } | ||
| } | ||
|
|
||
| export async function PUT(request) { | ||
| try { | ||
| const body = await request.json(); | ||
|
|
||
| if (body.prompt !== undefined && typeof body.prompt !== "string") { | ||
| return NextResponse.json({ error: "prompt must be a string" }, { status: 400 }); | ||
| } | ||
|
|
||
| setSystemPromptConfig(body); | ||
| await updateSettings({ systemPrompt: body }); | ||
|
|
||
| return NextResponse.json(getSystemPromptConfig()); | ||
| } catch (error) { | ||
| return NextResponse.json({ error: error.message }, { status: 500 }); | ||
| } | ||
| } |
There was a problem hiding this comment.
The system prompt injection API lacks authentication. An attacker could inject malicious system prompts that would be prepended to all user requests, potentially causing the AI to behave in harmful ways or leak sensitive information. This is a critical security vulnerability that must be addressed by adding proper authentication.
| if (isNaN(budget) || budget < 0 || budget > 131072) { | ||
| return NextResponse.json( | ||
| { error: "customBudget must be between 0 and 131072" }, | ||
| { status: 400 } | ||
| ); |
There was a problem hiding this comment.
The validation allows customBudget to be 0 (line 34), but in the setCustomBudget function in thinkingBudget.js (line 110-112), a budget of 0 sets thinking type to "disabled". While this works correctly, it would be clearer if the validation comment or error message explicitly mentioned that 0 means "disabled" rather than just stating the range as "between 0 and 131072".
| const regexStr = pattern | ||
| .replace(/[.+^${}()|[\]\\]/g, "\\$&") | ||
| .replace(/\*/g, ".*") | ||
| .replace(/\?/g, "."); | ||
| const regex = new RegExp(`^${regexStr}$`, "i"); | ||
| return regex.test(model); |
There was a problem hiding this comment.
The wildcard pattern conversion to regex (lines 22-26) could be vulnerable to ReDoS attacks if an attacker provides malicious patterns with many wildcards. Consider adding input validation to limit the number of wildcards allowed in a pattern (e.g., max 5 wildcards) or pattern length limits. While this is primarily used for admin-configured aliases, defense in depth is recommended.
| @@ -1,5 +1,6 @@ | |||
| /** @type {import('next').NextConfig} */ | |||
| const nextConfig = { | |||
There was a problem hiding this comment.
The turbopack: {} configuration is added but appears to be an empty object. Turbopack is Next.js's new bundler (still in beta). If this is intentional to enable Turbopack in development, consider adding a comment explaining its purpose. If it's not being used, this line can be removed. Also verify that all features work correctly with Turbopack enabled, as it may have compatibility issues with some packages or features.
| const nextConfig = { | |
| const nextConfig = { | |
| // Enable Turbopack (Next.js experimental bundler) in development with default settings. | |
| // Verify compatibility of all features/packages before relying on this in production. |
| export async function GET() { | ||
| try { | ||
| return NextResponse.json(getIPFilterConfig()); | ||
| } catch (error) { | ||
| return NextResponse.json({ error: error.message }, { status: 500 }); | ||
| } | ||
| } | ||
|
|
||
| export async function PUT(request) { | ||
| try { | ||
| const body = await request.json(); | ||
|
|
||
| // Configure entire filter | ||
| if (body.enabled !== undefined || body.mode || body.blacklist || body.whitelist) { | ||
| configureIPFilter(body); | ||
| } | ||
|
|
||
| // Add/remove individual IPs | ||
| if (body.addBlacklist) addToBlacklist(body.addBlacklist); | ||
| if (body.removeBlacklist) removeFromBlacklist(body.removeBlacklist); | ||
| if (body.addWhitelist) addToWhitelist(body.addWhitelist); | ||
| if (body.removeWhitelist) removeFromWhitelist(body.removeWhitelist); | ||
|
|
||
| // Temp bans | ||
| if (body.tempBan) { | ||
| tempBanIP(body.tempBan.ip, body.tempBan.durationMs || 3600000, body.tempBan.reason || "Manual ban"); | ||
| } | ||
| if (body.removeBan) removeTempBan(body.removeBan); | ||
|
|
||
| return NextResponse.json(getIPFilterConfig()); | ||
| } catch (error) { | ||
| return NextResponse.json({ error: error.message }, { status: 500 }); | ||
| } | ||
| } |
There was a problem hiding this comment.
The IP filter configuration API (/api/settings/ip-filter) has no authentication or authorization checks. This is a critical security endpoint that can lock out all users or allow unauthorized access. This endpoint should require authentication (e.g., admin login) before allowing IP filter configuration changes. The same concern applies to /api/settings/thinking-budget and /api/settings/system-prompt routes.
| export async function GET() { | ||
| try { | ||
| const config = getThinkingBudgetConfig(); | ||
| return NextResponse.json(config); | ||
| } catch (error) { | ||
| return NextResponse.json({ error: error.message }, { status: 500 }); | ||
| } | ||
| } | ||
|
|
||
| export async function PUT(request) { | ||
| try { | ||
| const body = await request.json(); | ||
|
|
||
| // Validate mode | ||
| const validModes = Object.values(ThinkingMode); | ||
| if (body.mode && !validModes.includes(body.mode)) { | ||
| return NextResponse.json( | ||
| { error: `Invalid mode. Must be one of: ${validModes.join(", ")}` }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Validate customBudget | ||
| if (body.customBudget !== undefined) { | ||
| const budget = parseInt(body.customBudget, 10); | ||
| if (isNaN(budget) || budget < 0 || budget > 131072) { | ||
| return NextResponse.json( | ||
| { error: "customBudget must be between 0 and 131072" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
| body.customBudget = budget; | ||
| } | ||
|
|
||
| // Validate effortLevel | ||
| const validEfforts = ["none", "low", "medium", "high"]; | ||
| if (body.effortLevel && !validEfforts.includes(body.effortLevel)) { | ||
| return NextResponse.json( | ||
| { error: `Invalid effortLevel. Must be one of: ${validEfforts.join(", ")}` }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Apply config in-memory | ||
| setThinkingBudgetConfig(body); | ||
|
|
||
| // Persist to settings DB | ||
| await updateSettings({ thinkingBudget: body }); | ||
|
|
||
| return NextResponse.json(getThinkingBudgetConfig()); | ||
| } catch (error) { | ||
| return NextResponse.json({ error: error.message }, { status: 500 }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Similar to the IP filter endpoint, this thinking budget configuration API has no authentication. An unauthorized user could change thinking budgets for all requests, potentially increasing costs significantly or degrading model performance. Add authentication middleware to protect this endpoint.
TLS / Certificate Validation (#50) - src/mitm/server.ts: rejectUnauthorized now defaults to true (opt-out via MITM_DISABLE_TLS_VERIFY=1 env var only) Path Injection (#44, #41-#49) - src/mitm/server.ts: safeLogPath() guards log filenames inside LOG_DIR - src/lib/db/backup.ts: path.resolve() anchor + sep/slash guard on backupId - src/shared/services/backupService.ts: safePath() helper anchors toolId and backupId within BACKUP_DIR (prevents path traversal) - src/app/api/cli-tools/codex-profiles/route.ts: safeProfilePath() helper anchors profileId within PROFILES_DIR Prototype Pollution (#18, #19, #20) - src/lib/usage/usageHistory.ts: byModel/byAccount initialized with Object.create(null); assignments guarded with hasOwnProperty checks Missing Workflow Permissions (#1-#8, #53) - .github/workflows/ci.yml: added global permissions: contents: read Dependabot CVE-2026-0540 (#16) - dompurify updated to ^3.3.2 (fixes XSS vulnerability)
docs: require fork-internal PRs by default (Hard Rule cheaper-inference#20)
… Rule #20 Rebased onto release/v3.8.39 (stripped v3.8.38 base-drift; the 3 intended files only). Adds the regression guard tests/unit/pii-opt-in-default.test.ts (definition defaults === "false" + behavioral pass-through), Hard Rule #20 in CLAUDE.md, and corrects the AGENTS.md guardrails blurb to state pii-masker is opt-in/OFF by default.
* chore(release): open v3.8.39 development cycle * docs(changelog): backfill 5 v3.8.38 bullets merged after release finalize These PRs squash-merged into release/v3.8.38 between the CHANGELOG finalize (ff57be3) and the merge-to-main (ae6e234), so they shipped in the v3.8.38 tag but had no bullet: - feat(compression): Ionizer engine (lossy JSON-array sampling + CCR) (#5148) - fix(sse): preserve non-stream reasoning fields (#5155, @rdself) - fix(i18n): add missing English UI labels (#5153, @rdself) - test(combo): gated live smoke (#5151) + release-expectations refresh (#5150, @KooshaPari) (#5129 exact-host Anthropic baseUrl is already covered by the #5130 bullet — same CodeQL #674.) Synced 41 i18n CHANGELOG mirrors. * feat(compression): TOON best-of-N candidate encoder + encoder A/B table (#5163) Integrated into release/v3.8.39. TOON best-of-N candidate encoder (GCF default, fail-open). 17/17 unit tests pass on merge result; CI reds were base-stale + Quality Ratchet DRIFT. * fix(zenmux): normalize vendor-prefixed GLM system roles (#5158) Integrated into release/v3.8.39. ZenMux vendor-prefixed GLM system-role normalization; 12/12 role-normalizer tests pass on merge result. CI reds base-stale. * [codex] fix xAI OAuth test and reasoning effort (#5157) Integrated into release/v3.8.39. xAI reasoning-effort normalization (max/xhigh→high) + OAuth test config; 46/46 xai-translator tests pass on merge result. CI reds base-stale. * docs(i18n): add Traditional Chinese (zh-TW) README and update zh-CN to latest (#5162) Integrated into release/v3.8.39. Traditional Chinese (zh-TW) README + zh-CN refresh; docs-only. * test(security): guard PII redaction stays opt-in (default off) + Hard Rule #20 (#5159) Integrated into release/v3.8.39. PII opt-in regression guard + Hard Rule #20; rebased to strip base-drift (+81/-1). 5/5 guard tests pass; flip-proof verified. * test(combo): deterministic context-relay universal-handoff coverage (closes phase-2 TODO) (#5168) Integrated into release/v3.8.39. Deterministic context-relay universal-handoff coverage (3 tests); 3/3 pass on merge result. * docs(i18n): full sync zh-TW and zh-CN README with canonical English v3.8.39 (#5171) Integrated into release/v3.8.39. Full zh-TW docs tree + zh-CN sync with canonical English v3.8.39; docs-only. * fix(serve): honour HOSTNAME from .env instead of hardcoding 0.0.0.0 (#5134) (#5170) Integrated into release/v3.8.39. HOSTNAME env override in serve (#5134) + regression test (4/4, TDD flip-proof verified). * fix(sse): resolve nameless deepseek-web tool blocks via parameter-schema match (#5154) (#5173) Integrated into release/v3.8.39. Schema-based nameless deepseek-web tool-block resolution (#5154); 6/6 tests pass on merge result (incl. ambiguous/no-match negatives + named-tag no-regression). * fix(sse): normalize array user content for Command Code to avoid upstream 400 (#5166) (#5174) Integrated into release/v3.8.39. Normalize array user content for Command Code (#5166, user-array/400 symptom); 4/4 tests pass on merge result. * fix(sse): defer </think> close so it never leaks before tool_calls (#5123) (#5175) Integrated into release/v3.8.39. Defer </think> close so it never leaks before tool_calls (#5123); 4/4 tests pass (incl. #4633 no-regression). CHANGELOG synced to keep all 3 v3.8.39 fixes. * fix(dashboard): use amber for home update-step warning icon (#5176) Integrated into release/v3.8.39. Amber for home update-step warning icon; 1/1 UI test. * fix(api): LAN/Tailscale dashboard — host-aware CSP + GET-exempt version route + combo field errors (#5083) (#5177) Integrated into release/v3.8.39. Host-aware CSP (ReDoS/injection-safe host validation) + GET-exempt /api/system/version (POST/spawn stays LOCAL_ONLY, exact-match safe-methods-only) + COMBO_002 firstField. 44/44 tests + route-guard membership gate green. CHANGELOG synced to keep all 4 v3.8.39 fixes. * fix(api): replace #5083 global middleware CSP with declarative ws: scheme (#5083) Follow-up to PR #5177 (merged): that version implemented the LAN-CSP fix (Bug 1) with a new global `src/middleware.ts` + `src/server/csp.ts`, which contradicts the project's documented architecture — 'No global Next.js middleware — interception is route-specific' (CLAUDE.md / AGENTS.md) — and was merged unverified (middleware vs next.config header precedence was never confirmed in a real build). This replaces that approach with the minimal, declarative equivalent: • next.config.mjs: connect-src now permits the bare `ws:` scheme (symmetric with the bare `wss:` already allowed) so the dashboard can reach its own Live WS server from a LAN/Tailscale host. No middleware. • Removes src/middleware.ts, src/server/csp.ts, and tests/unit/csp-host-aware.test.ts. • Adds tests/unit/csp-lan-ws-5083.test.ts (incl. a guard asserting src/middleware.ts does NOT exist, so the global-middleware approach cannot silently return). Bugs 2 (GET-exempt /api/system/version) and 3 (COMBO_002 field surfacing) from #5177 are unaffected and remain in place. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> * test(combo): end-to-end quota-share DRR routing-decision coverage (matrix parity) (#5179) Integrated into release/v3.8.39. Quota-share DRR routing-decision coverage (matrix parity); 2/2 pass on merge result. * feat(agent-bridge): graceful cert-install fallback with manual guide for containers (#4546) (#5178) Integrated into release/v3.8.39. Agent-bridge graceful cert-install fallback + manual guide (#4546); 6/6 tests pass on merge result. * fix(antigravity): family-scoped quota lockout (gemini/claude buckets) (#5180) Integrated into release/v3.8.39 — family-scoped antigravity quota lockout. Rebased from v3.8.37 + validated (vitest 5/5, typecheck clean, full combo-matrix green, model-lockout 99/0). Same-model cross-account retry (chat.ts) deferred pending live antigravity VPS validation. * fix(cli): force NODE_ENV to match dev/start run mode in custom Next server (#5189) Integrated into release/v3.8.39. Force NODE_ENV to match dev/start run mode in custom Next server; 2/2 source-scan+ordering tests pass on merge result. * feat(compression): CCR ranged/grep/stats retrieval (ReDoS-safe, backward-compat) (#5187) Integrated into release/v3.8.39. CCR ranged/grep/stats retrieval (safe-regex ReDoS guard + length/match caps); 17/17 tests pass on merge result. * docs(combo): sync all combo/routing-strategy docs to current state + document test coverage (#5185) Integrated into release/v3.8.39. Combo/routing-strategy docs sync; docs-only. * fix(mcp): return 404 (not 400) for unknown Streamable HTTP session id (#5169) (#5191) * fix(api): respect blocked Auto (Zero-Config) provider in /v1/models catalog (#5192) (#5194) * test(combo): deterministic context-relay codex quota-handoff coverage (closes last gap) (#5195) * test(ci): wire antigravity-quota-family under test:vitest (fix test-discovery orphan) (#5196) * fix(oauth): antigravity login no longer hangs — fire-and-forget onboarding + bounded post-exchange (#5193) Antigravity OAuth hang fix (no-PKCE/no-openid + bounded post-exchange + exchange-500 fix). Includes #5200 (Koosha) revert + owner rebaseline to keep documented comments. Integrated into release/v3.8.39. * feat(oauth): remote Antigravity login via local helper + paste-credentials (#5203) Remote Antigravity login: local helper (omniroute login antigravity) + paste-credentials. Integrated into release/v3.8.39. * fix(translator): accept Claude Messages shape in non-stream malformed-200 guard (#5156) Integrated into release/v3.8.39 * fix(cli): default dev bundler to Turbopack (16.2.x panic no longer reproduces) (#5206) Integrated into release/v3.8.39 * fix(cli): auto-calibrate server V8 heap from physical RAM (#5172) (#5213) The server was spawned with a fixed --max-old-space-size=512 (omniroute serve) or no heap flag at all (Electron), so RAM-rich boxes still OOM-crashed under load (Ineffective mark-compacts near heap limit ~500MB) with many providers/ accounts and large model catalogs. New calibrateHeapFallbackMb(os.totalmem()) defaults the heap to ~35% of RAM clamped [512,4096], wired into serve.mjs and electron/main.js. Explicit OMNIROUTE_MEMORY_MB still wins (#2939 unchanged). Also addresses #5160 (same OOM root); #5152 (docker) benefits via the same knob. Closes #5172 * fix(proxy): coalesce fast-fail health probes (#5208) Integrated into release/v3.8.39 * fix(proxy): close dispatchers when clearing cache (#5202) Integrated into release/v3.8.39 * fix(cli): raise dev server Node heap limit to 8GB to prevent OOM (#5198) Integrated into release/v3.8.39 * fix(auth): allow synthetic no-auth fallback for mimocode (#5205) Integrated into release/v3.8.39 * fix(oauth): preserve Antigravity refresh_token on empty/omitted upstream response (#3850) (#5214) Google's OAuth refresh tokens are non-rotating: the refresh response usually omits refresh_token and occasionally returns it as an empty string. The Antigravity executor used `typeof tokens.refresh_token === "string" ? ... ` which accepts "" (typeof "" === "string") and overwrote the stored token with empty, nulling it on first refresh. Now treats non-string OR empty as absent and preserves credentials.refreshToken, matching refreshGoogleToken semantics. Closes #3850 * fix(responses): normalize non-array input (#5204) Integrated into release/v3.8.39 * fix(stream): normalize safety finish reasons via shared helper (#5197) Integrated into release/v3.8.39 * fix(request-logger): never render negative '(-100%)' compression badge (#5201) Integrated into release/v3.8.39 * fix(combo): reject empty responses api output (#5207) Integrated into release/v3.8.39 — combo failover now rejects empty Responses API output (validateQuality). Baseline rebaseline dropped (main-measured drift; maintainer rebaselines at release). * fix(pwa): prefer cached navigation before offline page (#5209) Integrated into release/v3.8.39 — PWA service worker prefers cached navigation before offline page (#5165). * chore(release): v3.8.39 — 2026-06-28 * chore(release): rebaseline openapi+i18n coverage ratchet drift for v3.8.39 --------- Co-authored-by: Arthur Bodera <abodera@gmail.com> Co-authored-by: Nguyen Minh <lop123thcs@gmail.com> Co-authored-by: lunkerchen <labanchen@gmail.com> Co-authored-by: Ankit <177378174+anki1kr@users.noreply.github.com> Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> Co-authored-by: Ardem2025 <ardemb22@gmail.com> Co-authored-by: backryun <bakryun0718@proton.me> Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com> Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Co-authored-by: Wilson <pedbookmed@gmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
* chore(release): open v3.8.39 development cycle * docs(changelog): backfill 5 v3.8.38 bullets merged after release finalize These PRs squash-merged into release/v3.8.38 between the CHANGELOG finalize (ff57be3) and the merge-to-main (ae6e234), so they shipped in the v3.8.38 tag but had no bullet: - feat(compression): Ionizer engine (lossy JSON-array sampling + CCR) (cheaper-inference#5148) - fix(sse): preserve non-stream reasoning fields (cheaper-inference#5155, @rdself) - fix(i18n): add missing English UI labels (cheaper-inference#5153, @rdself) - test(combo): gated live smoke (cheaper-inference#5151) + release-expectations refresh (cheaper-inference#5150, @KooshaPari) (cheaper-inference#5129 exact-host Anthropic baseUrl is already covered by the cheaper-inference#5130 bullet — same CodeQL cheaper-inference#674.) Synced 41 i18n CHANGELOG mirrors. * feat(compression): TOON best-of-N candidate encoder + encoder A/B table (cheaper-inference#5163) Integrated into release/v3.8.39. TOON best-of-N candidate encoder (GCF default, fail-open). 17/17 unit tests pass on merge result; CI reds were base-stale + Quality Ratchet DRIFT. * fix(zenmux): normalize vendor-prefixed GLM system roles (cheaper-inference#5158) Integrated into release/v3.8.39. ZenMux vendor-prefixed GLM system-role normalization; 12/12 role-normalizer tests pass on merge result. CI reds base-stale. * [codex] fix xAI OAuth test and reasoning effort (cheaper-inference#5157) Integrated into release/v3.8.39. xAI reasoning-effort normalization (max/xhigh→high) + OAuth test config; 46/46 xai-translator tests pass on merge result. CI reds base-stale. * docs(i18n): add Traditional Chinese (zh-TW) README and update zh-CN to latest (cheaper-inference#5162) Integrated into release/v3.8.39. Traditional Chinese (zh-TW) README + zh-CN refresh; docs-only. * test(security): guard PII redaction stays opt-in (default off) + Hard Rule cheaper-inference#20 (cheaper-inference#5159) Integrated into release/v3.8.39. PII opt-in regression guard + Hard Rule cheaper-inference#20; rebased to strip base-drift (+81/-1). 5/5 guard tests pass; flip-proof verified. * test(combo): deterministic context-relay universal-handoff coverage (closes phase-2 TODO) (cheaper-inference#5168) Integrated into release/v3.8.39. Deterministic context-relay universal-handoff coverage (3 tests); 3/3 pass on merge result. * docs(i18n): full sync zh-TW and zh-CN README with canonical English v3.8.39 (cheaper-inference#5171) Integrated into release/v3.8.39. Full zh-TW docs tree + zh-CN sync with canonical English v3.8.39; docs-only. * fix(serve): honour HOSTNAME from .env instead of hardcoding 0.0.0.0 (cheaper-inference#5134) (cheaper-inference#5170) Integrated into release/v3.8.39. HOSTNAME env override in serve (cheaper-inference#5134) + regression test (4/4, TDD flip-proof verified). * fix(sse): resolve nameless deepseek-web tool blocks via parameter-schema match (cheaper-inference#5154) (cheaper-inference#5173) Integrated into release/v3.8.39. Schema-based nameless deepseek-web tool-block resolution (cheaper-inference#5154); 6/6 tests pass on merge result (incl. ambiguous/no-match negatives + named-tag no-regression). * fix(sse): normalize array user content for Command Code to avoid upstream 400 (cheaper-inference#5166) (cheaper-inference#5174) Integrated into release/v3.8.39. Normalize array user content for Command Code (cheaper-inference#5166, user-array/400 symptom); 4/4 tests pass on merge result. * fix(sse): defer </think> close so it never leaks before tool_calls (cheaper-inference#5123) (cheaper-inference#5175) Integrated into release/v3.8.39. Defer </think> close so it never leaks before tool_calls (cheaper-inference#5123); 4/4 tests pass (incl. cheaper-inference#4633 no-regression). CHANGELOG synced to keep all 3 v3.8.39 fixes. * fix(dashboard): use amber for home update-step warning icon (cheaper-inference#5176) Integrated into release/v3.8.39. Amber for home update-step warning icon; 1/1 UI test. * fix(api): LAN/Tailscale dashboard — host-aware CSP + GET-exempt version route + combo field errors (cheaper-inference#5083) (cheaper-inference#5177) Integrated into release/v3.8.39. Host-aware CSP (ReDoS/injection-safe host validation) + GET-exempt /api/system/version (POST/spawn stays LOCAL_ONLY, exact-match safe-methods-only) + COMBO_002 firstField. 44/44 tests + route-guard membership gate green. CHANGELOG synced to keep all 4 v3.8.39 fixes. * fix(api): replace cheaper-inference#5083 global middleware CSP with declarative ws: scheme (cheaper-inference#5083) Follow-up to PR cheaper-inference#5177 (merged): that version implemented the LAN-CSP fix (Bug 1) with a new global `src/middleware.ts` + `src/server/csp.ts`, which contradicts the project's documented architecture — 'No global Next.js middleware — interception is route-specific' (CLAUDE.md / AGENTS.md) — and was merged unverified (middleware vs next.config header precedence was never confirmed in a real build). This replaces that approach with the minimal, declarative equivalent: • next.config.mjs: connect-src now permits the bare `ws:` scheme (symmetric with the bare `wss:` already allowed) so the dashboard can reach its own Live WS server from a LAN/Tailscale host. No middleware. • Removes src/middleware.ts, src/server/csp.ts, and tests/unit/csp-host-aware.test.ts. • Adds tests/unit/csp-lan-ws-5083.test.ts (incl. a guard asserting src/middleware.ts does NOT exist, so the global-middleware approach cannot silently return). Bugs 2 (GET-exempt /api/system/version) and 3 (COMBO_002 field surfacing) from cheaper-inference#5177 are unaffected and remain in place. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> * test(combo): end-to-end quota-share DRR routing-decision coverage (matrix parity) (cheaper-inference#5179) Integrated into release/v3.8.39. Quota-share DRR routing-decision coverage (matrix parity); 2/2 pass on merge result. * feat(agent-bridge): graceful cert-install fallback with manual guide for containers (cheaper-inference#4546) (cheaper-inference#5178) Integrated into release/v3.8.39. Agent-bridge graceful cert-install fallback + manual guide (cheaper-inference#4546); 6/6 tests pass on merge result. * fix(antigravity): family-scoped quota lockout (gemini/claude buckets) (cheaper-inference#5180) Integrated into release/v3.8.39 — family-scoped antigravity quota lockout. Rebased from v3.8.37 + validated (vitest 5/5, typecheck clean, full combo-matrix green, model-lockout 99/0). Same-model cross-account retry (chat.ts) deferred pending live antigravity VPS validation. * fix(cli): force NODE_ENV to match dev/start run mode in custom Next server (cheaper-inference#5189) Integrated into release/v3.8.39. Force NODE_ENV to match dev/start run mode in custom Next server; 2/2 source-scan+ordering tests pass on merge result. * feat(compression): CCR ranged/grep/stats retrieval (ReDoS-safe, backward-compat) (cheaper-inference#5187) Integrated into release/v3.8.39. CCR ranged/grep/stats retrieval (safe-regex ReDoS guard + length/match caps); 17/17 tests pass on merge result. * docs(combo): sync all combo/routing-strategy docs to current state + document test coverage (cheaper-inference#5185) Integrated into release/v3.8.39. Combo/routing-strategy docs sync; docs-only. * fix(mcp): return 404 (not 400) for unknown Streamable HTTP session id (cheaper-inference#5169) (cheaper-inference#5191) * fix(api): respect blocked Auto (Zero-Config) provider in /v1/models catalog (cheaper-inference#5192) (cheaper-inference#5194) * test(combo): deterministic context-relay codex quota-handoff coverage (closes last gap) (cheaper-inference#5195) * test(ci): wire antigravity-quota-family under test:vitest (fix test-discovery orphan) (cheaper-inference#5196) * fix(oauth): antigravity login no longer hangs — fire-and-forget onboarding + bounded post-exchange (cheaper-inference#5193) Antigravity OAuth hang fix (no-PKCE/no-openid + bounded post-exchange + exchange-500 fix). Includes cheaper-inference#5200 (Koosha) revert + owner rebaseline to keep documented comments. Integrated into release/v3.8.39. * feat(oauth): remote Antigravity login via local helper + paste-credentials (cheaper-inference#5203) Remote Antigravity login: local helper (omniroute login antigravity) + paste-credentials. Integrated into release/v3.8.39. * fix(translator): accept Claude Messages shape in non-stream malformed-200 guard (cheaper-inference#5156) Integrated into release/v3.8.39 * fix(cli): default dev bundler to Turbopack (16.2.x panic no longer reproduces) (cheaper-inference#5206) Integrated into release/v3.8.39 * fix(cli): auto-calibrate server V8 heap from physical RAM (cheaper-inference#5172) (cheaper-inference#5213) The server was spawned with a fixed --max-old-space-size=512 (omniroute serve) or no heap flag at all (Electron), so RAM-rich boxes still OOM-crashed under load (Ineffective mark-compacts near heap limit ~500MB) with many providers/ accounts and large model catalogs. New calibrateHeapFallbackMb(os.totalmem()) defaults the heap to ~35% of RAM clamped [512,4096], wired into serve.mjs and electron/main.js. Explicit OMNIROUTE_MEMORY_MB still wins (cheaper-inference#2939 unchanged). Also addresses cheaper-inference#5160 (same OOM root); cheaper-inference#5152 (docker) benefits via the same knob. Closes cheaper-inference#5172 * fix(proxy): coalesce fast-fail health probes (cheaper-inference#5208) Integrated into release/v3.8.39 * fix(proxy): close dispatchers when clearing cache (cheaper-inference#5202) Integrated into release/v3.8.39 * fix(cli): raise dev server Node heap limit to 8GB to prevent OOM (cheaper-inference#5198) Integrated into release/v3.8.39 * fix(auth): allow synthetic no-auth fallback for mimocode (cheaper-inference#5205) Integrated into release/v3.8.39 * fix(oauth): preserve Antigravity refresh_token on empty/omitted upstream response (cheaper-inference#3850) (cheaper-inference#5214) Google's OAuth refresh tokens are non-rotating: the refresh response usually omits refresh_token and occasionally returns it as an empty string. The Antigravity executor used `typeof tokens.refresh_token === "string" ? ... ` which accepts "" (typeof "" === "string") and overwrote the stored token with empty, nulling it on first refresh. Now treats non-string OR empty as absent and preserves credentials.refreshToken, matching refreshGoogleToken semantics. Closes cheaper-inference#3850 * fix(responses): normalize non-array input (cheaper-inference#5204) Integrated into release/v3.8.39 * fix(stream): normalize safety finish reasons via shared helper (cheaper-inference#5197) Integrated into release/v3.8.39 * fix(request-logger): never render negative '(-100%)' compression badge (cheaper-inference#5201) Integrated into release/v3.8.39 * fix(combo): reject empty responses api output (cheaper-inference#5207) Integrated into release/v3.8.39 — combo failover now rejects empty Responses API output (validateQuality). Baseline rebaseline dropped (main-measured drift; maintainer rebaselines at release). * fix(pwa): prefer cached navigation before offline page (cheaper-inference#5209) Integrated into release/v3.8.39 — PWA service worker prefers cached navigation before offline page (cheaper-inference#5165). * chore(release): v3.8.39 — 2026-06-28 * chore(release): rebaseline openapi+i18n coverage ratchet drift for v3.8.39 --------- Co-authored-by: Arthur Bodera <abodera@gmail.com> Co-authored-by: Nguyen Minh <lop123thcs@gmail.com> Co-authored-by: lunkerchen <labanchen@gmail.com> Co-authored-by: Ankit <177378174+anki1kr@users.noreply.github.com> Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> Co-authored-by: Ardem2025 <ardemb22@gmail.com> Co-authored-by: backryun <bakryun0718@proton.me> Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com> Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Co-authored-by: Wilson <pedbookmed@gmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
…peline (#7339) (#7736) Phases 3-4 of #3384 (Phases 1-2 shipped DB schema + resolveInterceptSearch in release/v3.8.47). Adds resolveInterceptFetch(provider, model) as a structural twin of resolveInterceptSearch, and open-sse/services/webFetchInterception.ts (mirroring webSearchFallback.ts) to rewrite a provider-native web_fetch tool declaration into a synthetic omniroute_web_fetch function tool. The synthetic tool call is dispatched through the existing handleToolCallExecution path (same as omniroute_web_search) to a new web_fetch builtin skill handler that resolves credentials and calls handleWebFetch() against /v1/web/fetch. Strictly opt-in: with no interceptFetch DB row configured (the default), the outgoing request body is byte-identical to pre-change behavior — no heuristic default bypass like the interceptSearch sibling, to guarantee zero overhead when disabled (Hard Rule #20). Also ships the dashboard toggle (owner decision, overriding the plan's backend-only recommendation): ProviderInterceptionSection.tsx on the provider detail page, backed by GET/PUT/DELETE /api/providers/[id]/interception-rules, covering both interceptSearch and interceptFetch from one control. chatCore.ts touch is minimal (frozen file): one resolver call + one prepareWebFetchFallbackBody call mirroring the existing interceptSearch block, plus threading provider/model into the existing handleToolCallExecution call.
* chore(release): open v3.8.39 development cycle * docs(changelog): backfill 5 v3.8.38 bullets merged after release finalize These PRs squash-merged into release/v3.8.38 between the CHANGELOG finalize (f6aa6c3) and the merge-to-main (e2efe2c), so they shipped in the v3.8.38 tag but had no bullet: - feat(compression): Ionizer engine (lossy JSON-array sampling + CCR) (cheaper-inference#5148) - fix(sse): preserve non-stream reasoning fields (cheaper-inference#5155, @rdself) - fix(i18n): add missing English UI labels (cheaper-inference#5153, @rdself) - test(combo): gated live smoke (cheaper-inference#5151) + release-expectations refresh (cheaper-inference#5150, @KooshaPari) (cheaper-inference#5129 exact-host Anthropic baseUrl is already covered by the cheaper-inference#5130 bullet — same CodeQL cheaper-inference#674.) Synced 41 i18n CHANGELOG mirrors. * feat(compression): TOON best-of-N candidate encoder + encoder A/B table (cheaper-inference#5163) Integrated into release/v3.8.39. TOON best-of-N candidate encoder (GCF default, fail-open). 17/17 unit tests pass on merge result; CI reds were base-stale + Quality Ratchet DRIFT. * fix(zenmux): normalize vendor-prefixed GLM system roles (cheaper-inference#5158) Integrated into release/v3.8.39. ZenMux vendor-prefixed GLM system-role normalization; 12/12 role-normalizer tests pass on merge result. CI reds base-stale. * [codex] fix xAI OAuth test and reasoning effort (cheaper-inference#5157) Integrated into release/v3.8.39. xAI reasoning-effort normalization (max/xhigh→high) + OAuth test config; 46/46 xai-translator tests pass on merge result. CI reds base-stale. * docs(i18n): add Traditional Chinese (zh-TW) README and update zh-CN to latest (cheaper-inference#5162) Integrated into release/v3.8.39. Traditional Chinese (zh-TW) README + zh-CN refresh; docs-only. * test(security): guard PII redaction stays opt-in (default off) + Hard Rule cheaper-inference#20 (cheaper-inference#5159) Integrated into release/v3.8.39. PII opt-in regression guard + Hard Rule cheaper-inference#20; rebased to strip base-drift (+81/-1). 5/5 guard tests pass; flip-proof verified. * test(combo): deterministic context-relay universal-handoff coverage (closes phase-2 TODO) (cheaper-inference#5168) Integrated into release/v3.8.39. Deterministic context-relay universal-handoff coverage (3 tests); 3/3 pass on merge result. * docs(i18n): full sync zh-TW and zh-CN README with canonical English v3.8.39 (cheaper-inference#5171) Integrated into release/v3.8.39. Full zh-TW docs tree + zh-CN sync with canonical English v3.8.39; docs-only. * fix(serve): honour HOSTNAME from .env instead of hardcoding 0.0.0.0 (cheaper-inference#5134) (cheaper-inference#5170) Integrated into release/v3.8.39. HOSTNAME env override in serve (cheaper-inference#5134) + regression test (4/4, TDD flip-proof verified). * fix(sse): resolve nameless deepseek-web tool blocks via parameter-schema match (cheaper-inference#5154) (cheaper-inference#5173) Integrated into release/v3.8.39. Schema-based nameless deepseek-web tool-block resolution (cheaper-inference#5154); 6/6 tests pass on merge result (incl. ambiguous/no-match negatives + named-tag no-regression). * fix(sse): normalize array user content for Command Code to avoid upstream 400 (cheaper-inference#5166) (cheaper-inference#5174) Integrated into release/v3.8.39. Normalize array user content for Command Code (cheaper-inference#5166, user-array/400 symptom); 4/4 tests pass on merge result. * fix(sse): defer </think> close so it never leaks before tool_calls (cheaper-inference#5123) (cheaper-inference#5175) Integrated into release/v3.8.39. Defer </think> close so it never leaks before tool_calls (cheaper-inference#5123); 4/4 tests pass (incl. cheaper-inference#4633 no-regression). CHANGELOG synced to keep all 3 v3.8.39 fixes. * fix(dashboard): use amber for home update-step warning icon (cheaper-inference#5176) Integrated into release/v3.8.39. Amber for home update-step warning icon; 1/1 UI test. * fix(api): LAN/Tailscale dashboard — host-aware CSP + GET-exempt version route + combo field errors (cheaper-inference#5083) (cheaper-inference#5177) Integrated into release/v3.8.39. Host-aware CSP (ReDoS/injection-safe host validation) + GET-exempt /api/system/version (POST/spawn stays LOCAL_ONLY, exact-match safe-methods-only) + COMBO_002 firstField. 44/44 tests + route-guard membership gate green. CHANGELOG synced to keep all 4 v3.8.39 fixes. * fix(api): replace cheaper-inference#5083 global middleware CSP with declarative ws: scheme (cheaper-inference#5083) Follow-up to PR cheaper-inference#5177 (merged): that version implemented the LAN-CSP fix (Bug 1) with a new global `src/middleware.ts` + `src/server/csp.ts`, which contradicts the project's documented architecture — 'No global Next.js middleware — interception is route-specific' (CLAUDE.md / AGENTS.md) — and was merged unverified (middleware vs next.config header precedence was never confirmed in a real build). This replaces that approach with the minimal, declarative equivalent: • next.config.mjs: connect-src now permits the bare `ws:` scheme (symmetric with the bare `wss:` already allowed) so the dashboard can reach its own Live WS server from a LAN/Tailscale host. No middleware. • Removes src/middleware.ts, src/server/csp.ts, and tests/unit/csp-host-aware.test.ts. • Adds tests/unit/csp-lan-ws-5083.test.ts (incl. a guard asserting src/middleware.ts does NOT exist, so the global-middleware approach cannot silently return). Bugs 2 (GET-exempt /api/system/version) and 3 (COMBO_002 field surfacing) from cheaper-inference#5177 are unaffected and remain in place. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> * test(combo): end-to-end quota-share DRR routing-decision coverage (matrix parity) (cheaper-inference#5179) Integrated into release/v3.8.39. Quota-share DRR routing-decision coverage (matrix parity); 2/2 pass on merge result. * feat(agent-bridge): graceful cert-install fallback with manual guide for containers (cheaper-inference#4546) (cheaper-inference#5178) Integrated into release/v3.8.39. Agent-bridge graceful cert-install fallback + manual guide (cheaper-inference#4546); 6/6 tests pass on merge result. * fix(antigravity): family-scoped quota lockout (gemini/claude buckets) (cheaper-inference#5180) Integrated into release/v3.8.39 — family-scoped antigravity quota lockout. Rebased from v3.8.37 + validated (vitest 5/5, typecheck clean, full combo-matrix green, model-lockout 99/0). Same-model cross-account retry (chat.ts) deferred pending live antigravity VPS validation. * fix(cli): force NODE_ENV to match dev/start run mode in custom Next server (cheaper-inference#5189) Integrated into release/v3.8.39. Force NODE_ENV to match dev/start run mode in custom Next server; 2/2 source-scan+ordering tests pass on merge result. * feat(compression): CCR ranged/grep/stats retrieval (ReDoS-safe, backward-compat) (cheaper-inference#5187) Integrated into release/v3.8.39. CCR ranged/grep/stats retrieval (safe-regex ReDoS guard + length/match caps); 17/17 tests pass on merge result. * docs(combo): sync all combo/routing-strategy docs to current state + document test coverage (cheaper-inference#5185) Integrated into release/v3.8.39. Combo/routing-strategy docs sync; docs-only. * fix(mcp): return 404 (not 400) for unknown Streamable HTTP session id (cheaper-inference#5169) (cheaper-inference#5191) * fix(api): respect blocked Auto (Zero-Config) provider in /v1/models catalog (cheaper-inference#5192) (cheaper-inference#5194) * test(combo): deterministic context-relay codex quota-handoff coverage (closes last gap) (cheaper-inference#5195) * test(ci): wire antigravity-quota-family under test:vitest (fix test-discovery orphan) (cheaper-inference#5196) * fix(oauth): antigravity login no longer hangs — fire-and-forget onboarding + bounded post-exchange (cheaper-inference#5193) Antigravity OAuth hang fix (no-PKCE/no-openid + bounded post-exchange + exchange-500 fix). Includes cheaper-inference#5200 (Koosha) revert + owner rebaseline to keep documented comments. Integrated into release/v3.8.39. * feat(oauth): remote Antigravity login via local helper + paste-credentials (cheaper-inference#5203) Remote Antigravity login: local helper (omniroute login antigravity) + paste-credentials. Integrated into release/v3.8.39. * fix(translator): accept Claude Messages shape in non-stream malformed-200 guard (cheaper-inference#5156) Integrated into release/v3.8.39 * fix(cli): default dev bundler to Turbopack (16.2.x panic no longer reproduces) (cheaper-inference#5206) Integrated into release/v3.8.39 * fix(cli): auto-calibrate server V8 heap from physical RAM (cheaper-inference#5172) (cheaper-inference#5213) The server was spawned with a fixed --max-old-space-size=512 (omniroute serve) or no heap flag at all (Electron), so RAM-rich boxes still OOM-crashed under load (Ineffective mark-compacts near heap limit ~500MB) with many providers/ accounts and large model catalogs. New calibrateHeapFallbackMb(os.totalmem()) defaults the heap to ~35% of RAM clamped [512,4096], wired into serve.mjs and electron/main.js. Explicit OMNIROUTE_MEMORY_MB still wins (cheaper-inference#2939 unchanged). Also addresses cheaper-inference#5160 (same OOM root); cheaper-inference#5152 (docker) benefits via the same knob. Closes cheaper-inference#5172 * fix(proxy): coalesce fast-fail health probes (cheaper-inference#5208) Integrated into release/v3.8.39 * fix(proxy): close dispatchers when clearing cache (cheaper-inference#5202) Integrated into release/v3.8.39 * fix(cli): raise dev server Node heap limit to 8GB to prevent OOM (cheaper-inference#5198) Integrated into release/v3.8.39 * fix(auth): allow synthetic no-auth fallback for mimocode (cheaper-inference#5205) Integrated into release/v3.8.39 * fix(oauth): preserve Antigravity refresh_token on empty/omitted upstream response (cheaper-inference#3850) (cheaper-inference#5214) Google's OAuth refresh tokens are non-rotating: the refresh response usually omits refresh_token and occasionally returns it as an empty string. The Antigravity executor used `typeof tokens.refresh_token === "string" ? ... ` which accepts "" (typeof "" === "string") and overwrote the stored token with empty, nulling it on first refresh. Now treats non-string OR empty as absent and preserves credentials.refreshToken, matching refreshGoogleToken semantics. Closes cheaper-inference#3850 * fix(responses): normalize non-array input (cheaper-inference#5204) Integrated into release/v3.8.39 * fix(stream): normalize safety finish reasons via shared helper (cheaper-inference#5197) Integrated into release/v3.8.39 * fix(request-logger): never render negative '(-100%)' compression badge (cheaper-inference#5201) Integrated into release/v3.8.39 * fix(combo): reject empty responses api output (cheaper-inference#5207) Integrated into release/v3.8.39 — combo failover now rejects empty Responses API output (validateQuality). Baseline rebaseline dropped (main-measured drift; maintainer rebaselines at release). * fix(pwa): prefer cached navigation before offline page (cheaper-inference#5209) Integrated into release/v3.8.39 — PWA service worker prefers cached navigation before offline page (cheaper-inference#5165). * chore(release): v3.8.39 — 2026-06-28 * chore(release): rebaseline openapi+i18n coverage ratchet drift for v3.8.39 --------- Co-authored-by: Arthur Bodera <abodera@gmail.com> Co-authored-by: Nguyen Minh <lop123thcs@gmail.com> Co-authored-by: lunkerchen <labanchen@gmail.com> Co-authored-by: Ankit <177378174+anki1kr@users.noreply.github.com> Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> Co-authored-by: Ardem2025 <ardemb22@gmail.com> Co-authored-by: backryun <bakryun0718@proton.me> Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com> Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Co-authored-by: Wilson <pedbookmed@gmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
…peline (cheaper-inference#7339) (cheaper-inference#7736) Phases 3-4 of cheaper-inference#3384 (Phases 1-2 shipped DB schema + resolveInterceptSearch in release/v3.8.47). Adds resolveInterceptFetch(provider, model) as a structural twin of resolveInterceptSearch, and open-sse/services/webFetchInterception.ts (mirroring webSearchFallback.ts) to rewrite a provider-native web_fetch tool declaration into a synthetic omniroute_web_fetch function tool. The synthetic tool call is dispatched through the existing handleToolCallExecution path (same as omniroute_web_search) to a new web_fetch builtin skill handler that resolves credentials and calls handleWebFetch() against /v1/web/fetch. Strictly opt-in: with no interceptFetch DB row configured (the default), the outgoing request body is byte-identical to pre-change behavior — no heuristic default bypass like the interceptSearch sibling, to guarantee zero overhead when disabled (Hard Rule cheaper-inference#20). Also ships the dashboard toggle (owner decision, overriding the plan's backend-only recommendation): ProviderInterceptionSection.tsx on the provider detail page, backed by GET/PUT/DELETE /api/providers/[id]/interception-rules, covering both interceptSearch and interceptFetch from one control. chatCore.ts touch is minimal (frozen file): one resolver call + one prepareWebFetchFallbackBody call mirroring the existing interceptSearch block, plus threading provider/model into the existing handleToolCallExecution call.
* chore(release): open v3.8.39 development cycle * docs(changelog): backfill 5 v3.8.38 bullets merged after release finalize These PRs squash-merged into release/v3.8.38 between the CHANGELOG finalize (ff57be3) and the merge-to-main (ae6e234), so they shipped in the v3.8.38 tag but had no bullet: - feat(compression): Ionizer engine (lossy JSON-array sampling + CCR) (cheaper-inference#5148) - fix(sse): preserve non-stream reasoning fields (cheaper-inference#5155, @rdself) - fix(i18n): add missing English UI labels (cheaper-inference#5153, @rdself) - test(combo): gated live smoke (cheaper-inference#5151) + release-expectations refresh (cheaper-inference#5150, @KooshaPari) (cheaper-inference#5129 exact-host Anthropic baseUrl is already covered by the cheaper-inference#5130 bullet — same CodeQL cheaper-inference#674.) Synced 41 i18n CHANGELOG mirrors. * feat(compression): TOON best-of-N candidate encoder + encoder A/B table (cheaper-inference#5163) Integrated into release/v3.8.39. TOON best-of-N candidate encoder (GCF default, fail-open). 17/17 unit tests pass on merge result; CI reds were base-stale + Quality Ratchet DRIFT. * fix(zenmux): normalize vendor-prefixed GLM system roles (cheaper-inference#5158) Integrated into release/v3.8.39. ZenMux vendor-prefixed GLM system-role normalization; 12/12 role-normalizer tests pass on merge result. CI reds base-stale. * [codex] fix xAI OAuth test and reasoning effort (cheaper-inference#5157) Integrated into release/v3.8.39. xAI reasoning-effort normalization (max/xhigh→high) + OAuth test config; 46/46 xai-translator tests pass on merge result. CI reds base-stale. * docs(i18n): add Traditional Chinese (zh-TW) README and update zh-CN to latest (cheaper-inference#5162) Integrated into release/v3.8.39. Traditional Chinese (zh-TW) README + zh-CN refresh; docs-only. * test(security): guard PII redaction stays opt-in (default off) + Hard Rule cheaper-inference#20 (cheaper-inference#5159) Integrated into release/v3.8.39. PII opt-in regression guard + Hard Rule cheaper-inference#20; rebased to strip base-drift (+81/-1). 5/5 guard tests pass; flip-proof verified. * test(combo): deterministic context-relay universal-handoff coverage (closes phase-2 TODO) (cheaper-inference#5168) Integrated into release/v3.8.39. Deterministic context-relay universal-handoff coverage (3 tests); 3/3 pass on merge result. * docs(i18n): full sync zh-TW and zh-CN README with canonical English v3.8.39 (cheaper-inference#5171) Integrated into release/v3.8.39. Full zh-TW docs tree + zh-CN sync with canonical English v3.8.39; docs-only. * fix(serve): honour HOSTNAME from .env instead of hardcoding 0.0.0.0 (cheaper-inference#5134) (cheaper-inference#5170) Integrated into release/v3.8.39. HOSTNAME env override in serve (cheaper-inference#5134) + regression test (4/4, TDD flip-proof verified). * fix(sse): resolve nameless deepseek-web tool blocks via parameter-schema match (cheaper-inference#5154) (cheaper-inference#5173) Integrated into release/v3.8.39. Schema-based nameless deepseek-web tool-block resolution (cheaper-inference#5154); 6/6 tests pass on merge result (incl. ambiguous/no-match negatives + named-tag no-regression). * fix(sse): normalize array user content for Command Code to avoid upstream 400 (cheaper-inference#5166) (cheaper-inference#5174) Integrated into release/v3.8.39. Normalize array user content for Command Code (cheaper-inference#5166, user-array/400 symptom); 4/4 tests pass on merge result. * fix(sse): defer </think> close so it never leaks before tool_calls (cheaper-inference#5123) (cheaper-inference#5175) Integrated into release/v3.8.39. Defer </think> close so it never leaks before tool_calls (cheaper-inference#5123); 4/4 tests pass (incl. cheaper-inference#4633 no-regression). CHANGELOG synced to keep all 3 v3.8.39 fixes. * fix(dashboard): use amber for home update-step warning icon (cheaper-inference#5176) Integrated into release/v3.8.39. Amber for home update-step warning icon; 1/1 UI test. * fix(api): LAN/Tailscale dashboard — host-aware CSP + GET-exempt version route + combo field errors (cheaper-inference#5083) (cheaper-inference#5177) Integrated into release/v3.8.39. Host-aware CSP (ReDoS/injection-safe host validation) + GET-exempt /api/system/version (POST/spawn stays LOCAL_ONLY, exact-match safe-methods-only) + COMBO_002 firstField. 44/44 tests + route-guard membership gate green. CHANGELOG synced to keep all 4 v3.8.39 fixes. * fix(api): replace cheaper-inference#5083 global middleware CSP with declarative ws: scheme (cheaper-inference#5083) Follow-up to PR cheaper-inference#5177 (merged): that version implemented the LAN-CSP fix (Bug 1) with a new global `src/middleware.ts` + `src/server/csp.ts`, which contradicts the project's documented architecture — 'No global Next.js middleware — interception is route-specific' (CLAUDE.md / AGENTS.md) — and was merged unverified (middleware vs next.config header precedence was never confirmed in a real build). This replaces that approach with the minimal, declarative equivalent: • next.config.mjs: connect-src now permits the bare `ws:` scheme (symmetric with the bare `wss:` already allowed) so the dashboard can reach its own Live WS server from a LAN/Tailscale host. No middleware. • Removes src/middleware.ts, src/server/csp.ts, and tests/unit/csp-host-aware.test.ts. • Adds tests/unit/csp-lan-ws-5083.test.ts (incl. a guard asserting src/middleware.ts does NOT exist, so the global-middleware approach cannot silently return). Bugs 2 (GET-exempt /api/system/version) and 3 (COMBO_002 field surfacing) from cheaper-inference#5177 are unaffected and remain in place. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> * test(combo): end-to-end quota-share DRR routing-decision coverage (matrix parity) (cheaper-inference#5179) Integrated into release/v3.8.39. Quota-share DRR routing-decision coverage (matrix parity); 2/2 pass on merge result. * feat(agent-bridge): graceful cert-install fallback with manual guide for containers (cheaper-inference#4546) (cheaper-inference#5178) Integrated into release/v3.8.39. Agent-bridge graceful cert-install fallback + manual guide (cheaper-inference#4546); 6/6 tests pass on merge result. * fix(antigravity): family-scoped quota lockout (gemini/claude buckets) (cheaper-inference#5180) Integrated into release/v3.8.39 — family-scoped antigravity quota lockout. Rebased from v3.8.37 + validated (vitest 5/5, typecheck clean, full combo-matrix green, model-lockout 99/0). Same-model cross-account retry (chat.ts) deferred pending live antigravity VPS validation. * fix(cli): force NODE_ENV to match dev/start run mode in custom Next server (cheaper-inference#5189) Integrated into release/v3.8.39. Force NODE_ENV to match dev/start run mode in custom Next server; 2/2 source-scan+ordering tests pass on merge result. * feat(compression): CCR ranged/grep/stats retrieval (ReDoS-safe, backward-compat) (cheaper-inference#5187) Integrated into release/v3.8.39. CCR ranged/grep/stats retrieval (safe-regex ReDoS guard + length/match caps); 17/17 tests pass on merge result. * docs(combo): sync all combo/routing-strategy docs to current state + document test coverage (cheaper-inference#5185) Integrated into release/v3.8.39. Combo/routing-strategy docs sync; docs-only. * fix(mcp): return 404 (not 400) for unknown Streamable HTTP session id (cheaper-inference#5169) (cheaper-inference#5191) * fix(api): respect blocked Auto (Zero-Config) provider in /v1/models catalog (cheaper-inference#5192) (cheaper-inference#5194) * test(combo): deterministic context-relay codex quota-handoff coverage (closes last gap) (cheaper-inference#5195) * test(ci): wire antigravity-quota-family under test:vitest (fix test-discovery orphan) (cheaper-inference#5196) * fix(oauth): antigravity login no longer hangs — fire-and-forget onboarding + bounded post-exchange (cheaper-inference#5193) Antigravity OAuth hang fix (no-PKCE/no-openid + bounded post-exchange + exchange-500 fix). Includes cheaper-inference#5200 (Koosha) revert + owner rebaseline to keep documented comments. Integrated into release/v3.8.39. * feat(oauth): remote Antigravity login via local helper + paste-credentials (cheaper-inference#5203) Remote Antigravity login: local helper (omniroute login antigravity) + paste-credentials. Integrated into release/v3.8.39. * fix(translator): accept Claude Messages shape in non-stream malformed-200 guard (cheaper-inference#5156) Integrated into release/v3.8.39 * fix(cli): default dev bundler to Turbopack (16.2.x panic no longer reproduces) (cheaper-inference#5206) Integrated into release/v3.8.39 * fix(cli): auto-calibrate server V8 heap from physical RAM (cheaper-inference#5172) (cheaper-inference#5213) The server was spawned with a fixed --max-old-space-size=512 (omniroute serve) or no heap flag at all (Electron), so RAM-rich boxes still OOM-crashed under load (Ineffective mark-compacts near heap limit ~500MB) with many providers/ accounts and large model catalogs. New calibrateHeapFallbackMb(os.totalmem()) defaults the heap to ~35% of RAM clamped [512,4096], wired into serve.mjs and electron/main.js. Explicit OMNIROUTE_MEMORY_MB still wins (cheaper-inference#2939 unchanged). Also addresses cheaper-inference#5160 (same OOM root); cheaper-inference#5152 (docker) benefits via the same knob. Closes cheaper-inference#5172 * fix(proxy): coalesce fast-fail health probes (cheaper-inference#5208) Integrated into release/v3.8.39 * fix(proxy): close dispatchers when clearing cache (cheaper-inference#5202) Integrated into release/v3.8.39 * fix(cli): raise dev server Node heap limit to 8GB to prevent OOM (cheaper-inference#5198) Integrated into release/v3.8.39 * fix(auth): allow synthetic no-auth fallback for mimocode (cheaper-inference#5205) Integrated into release/v3.8.39 * fix(oauth): preserve Antigravity refresh_token on empty/omitted upstream response (cheaper-inference#3850) (cheaper-inference#5214) Google's OAuth refresh tokens are non-rotating: the refresh response usually omits refresh_token and occasionally returns it as an empty string. The Antigravity executor used `typeof tokens.refresh_token === "string" ? ... ` which accepts "" (typeof "" === "string") and overwrote the stored token with empty, nulling it on first refresh. Now treats non-string OR empty as absent and preserves credentials.refreshToken, matching refreshGoogleToken semantics. Closes cheaper-inference#3850 * fix(responses): normalize non-array input (cheaper-inference#5204) Integrated into release/v3.8.39 * fix(stream): normalize safety finish reasons via shared helper (cheaper-inference#5197) Integrated into release/v3.8.39 * fix(request-logger): never render negative '(-100%)' compression badge (cheaper-inference#5201) Integrated into release/v3.8.39 * fix(combo): reject empty responses api output (cheaper-inference#5207) Integrated into release/v3.8.39 — combo failover now rejects empty Responses API output (validateQuality). Baseline rebaseline dropped (main-measured drift; maintainer rebaselines at release). * fix(pwa): prefer cached navigation before offline page (cheaper-inference#5209) Integrated into release/v3.8.39 — PWA service worker prefers cached navigation before offline page (cheaper-inference#5165). * chore(release): v3.8.39 — 2026-06-28 * chore(release): rebaseline openapi+i18n coverage ratchet drift for v3.8.39 --------- Co-authored-by: Arthur Bodera <abodera@gmail.com> Co-authored-by: Nguyen Minh <lop123thcs@gmail.com> Co-authored-by: lunkerchen <labanchen@gmail.com> Co-authored-by: Ankit <177378174+anki1kr@users.noreply.github.com> Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> Co-authored-by: Ardem2025 <ardemb22@gmail.com> Co-authored-by: backryun <bakryun0718@proton.me> Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com> Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Co-authored-by: Wilson <pedbookmed@gmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
…actually shipped Four suites on the release tip pin a literal count that #9515 (radar) and #9009 (UnoRouter) moved. Each pin was re-derived from the source of truth, not relaxed: - tests/unit/feature-flags-settings.test.ts 44 -> 45 flags. #9515 added RADAR_ENABLED to src/shared/constants/featureFlagDefinitions.ts (defaultValue 'false', so Hard Rule #20's opt-in guarantee is untouched). 49/49. - tests/unit/sidebar-costs-section.test.ts 5 -> 6 costs items. #9515 appended { id: 'radar', href: '/dashboard/radar' } to the costs section in sidebarVisibility/sections.ts; both the order list and the href list now carry it in its real (last) position. 7/7. - tests/unit/providers-constants-split.test.ts 196 -> 197 APIKEY_PROVIDERS. #9009 added UnoRouter to the gateways family. The partition assertion moves with it, so the no-loss/no-dup invariant still holds. 4/4. - tests/unit/i18n-vi-completeness.test.ts: fixed the CAUSE instead of the pin — translated the 3 keys the Vietnamese locale was missing (providers.onboardingProviderDescriptions.unorouter, agentBridge.noMappingsDesc, agentBridge.addMapping). 5/5. Refs #9298
…e memory.db) (#16) * feat(memory): standalone four-layer memory storage core - New src/memory/ modules: db/core (lazy singleton + WAL), db/migrations (5 idempotent SQL files), l0/l1/l2/l3 repositories, operations (task_queue, task_lock, memory_settings, embedding_meta), retrieval/rrf (k=60 pure fusion), vectorStore (sqlite-vec best-effort). - Public surface via src/memory/index.ts. - 78 focused node:test cases under tests/unit/memory-core/ covering DB lifecycle (WAL PRAGMAs, idempotent migrations, independent file), all four layer CRUD/version/delete semantics, L2 max-15 cap, L3 singleton persona continuity, queue claim/DLQ/locks/settings, FTS5 retrieval, pure RRF fusion, owner scoping and SQL-injection safety. * feat(dashboard): rewrite /dashboard/memory four-layer UI with settings Replace legacy memories/playground/engine tabs with L0/L1/L2/L3/Settings tabs that match the four-layer memory model. URL query params drive the active tab and an optional lineage filter (survives reload/share); the chip exposes a clear-filter action. L0 surfaces raw, immutable conversation rows (session id, timestamp, role, provider, model, collapsible content). Internal markers (is_internal / combo_execution_key) are deliberately hidden. Recycle bin rows support restore and double-confirmed permanent delete. L1 lists distilled memory entries with seven localized type chips, priority, content, scene link, version, last-modified-by, and an edited marker. Edit / add / soft delete / permanent delete / restore / view-source / view-scene actions wire to the new /api/memory/l1/* endpoints. Score is rendered only when the API returns one. L2 lists scenes (heat, times, summary, version, modifier, content, pending status) with edit / delete / regenerate actions and links to atoms + persona. L3 lists prompts (mode chat|code, content bounded to 2000 chars, version, modifier) with edit / clear / regenerate / lineage links back to L1 and L2. Settings picks the distillation model with an effective value, source-layer badge (per-key → global → env → auto), provider/model override (scope self or global), PUT/DELETE on /api/memory/distillation-model. Status card surfaces running state, recent error, and DLQ retry. Sync-ed-models dropdown drives the override picker. All hooks are abort-safe and typed; errors are surfaced via state rather than thrown. UI uses the Apple premium components (AppleCard / AppleButton / AppleSurface / AppleField / AppleSelect / AppleStatusDot) consistent with the recent provider-card / api-manager work. i18n keys added under memory.l0/l1/l2/l3/ distillation/common and tabs (l0/l1/l2/l3/settings). Legacy tabs and their UI tests are removed. * feat(sse,memory): pipeline L0 capture + 4-layer recall/injection hard cutover Replace legacy extractFacts path with a new per-API-key pipeline that defaults both capture and injection to OFF, with the legacy memoryEnabled flag honored ONLY as an explicit migration path. L0 capture persists last user + assistant visible text (no 64KB cap), strips code blocks, schedules async writes via setImmediate (fail-open, no retry), enqueues L1 on success, and inserts two stable idempotency-derived records per turn. The 4-layer recall facade (L3 stable + L2 nav <=15 + L1 dynamic top-5 + static tools guide) is rendered into the leading system suffix and a per-turn <relevant-memories> user prefix, honoring cache-safe placement, system-must-be-first providers, and the GLM/o1 user-role fallback. Existing cheaper-inference#3890/cheaper-inference#6135/cheaper-inference#1701 tests remain green; new focused tests cover the full surface. * docs(memory): document v4.0 four-layer hard cutover Rewrite MEMORY.md and the related doc surfaces (API_REFERENCE, openapi, ENVIRONMENT, README, CLAUDE.md, CHANGELOG) to describe the canonical four-layer architecture (L0 raw turns / L1 typed facts / L2 navigation / L3 working singleton) on a standalone <DATA_DIR>/memory.db WAL with no backup, reset-by-delete semantics. Capture and injection are documented as default-off and per-API-key, PII feature flags remain opt-in per Hard Rule cheaper-inference#20, and the new x-omniroute-no-memory header is documented alongside the internal marker. Distillation (selector chain + DLQ at /api/memory/distillation/dlq), retention defaults, and the fork-only publication notice are all captured. The new API routes (/api/memory/{l0,l1,l2,l3,regenerate, distillation-model,distillation/dlq}) replace the legacy memory CRUD plus Qdrant/engine/health/reindex/retrieve-preview/summarize routes with no migration or compatibility shim. Scope: docs only. Source, tests, package metadata, license, third-party notices and the in-progress src/lib/memory implementation are intentionally untouched in this commit. * feat(memory): distillation worker/selector/permit — native four-layer service Native modules for the four-layer distillation service and worker. Scope is strictly isolated: src/memory/distillation/** config.ts — env resolution; doubly opt-in (ENABLED+INTERVAL>0); rejects isBuildPhase/isCloud/isAutomatedTestProcess; secret handling for internal marker HMAC. store.ts — DistillationStore interface + InMemoryDistillationStore; createDefaultDistillationStore() uses dynamic imports for the future @/memory/db/repositories adapter and falls back to in-memory dev/test. permit.ts — ProcessPermitPool: nonblocking acquire, TTL auto release, no permit leak. scheduler.ts — pure helpers: warm-up ramp 1→2→4→5, idle 90s, L2 90s delay + 15min/60min debounce, L3 immediate, retry 5/15/45 max 3 → DLQ. selector.ts — 4-tier chain (task hint, per-key, global, env, first active). No silent fallback after selection. failure.ts — classifier + retry/DLQ decision + sanitized error messages (strips stacks/Bearer tokens/api keys/paths). internalMarker.ts — X-Omniroute-No-Memory + HMAC-SHA256(secret, nonce|time) sign/verify, depth/call budget caps, loopback origin verification (127.0.0.1, ::1, trusted tokens). lock.ts — owner-level mutual exclusion (TTL 240s, renew 30s). usage.ts — UsageBatcher (flushes on size or interval, never drops on shutdown) + buildUsageRecord helper. executor.ts — plain nonstream runner, breaker seam, is_internal metadata, never trips production breaker. handlers.ts — L0/L1/L2/L3 default handlers with actual orchestration + JSON parsing + regex fallback for L0 evidence; Tencent prompts behind dynamic import. worker.ts — startDistillationWorker/stopDistillationWorker lifecycle, setTimeout initial + setInterval env-driven, unref, synchronous reentry guard, scope owner-lock, no silent provider/model switch, breaker-open re-queues without burning an attempt, model_unset/model_deleted/ credentials_invalid → DLQ. public.ts — barrel re-exports. tests/unit/memory/distillation/ config, permit, scheduler, store, selector, internalMarker, failure, lock, usage, handlers, worker-lifecycle — 11 focused unit test files covering env gates, lifecycle no-start, no-overlap, permit TTL, scheduling, selector 4-tier fallback, no runtime auto fallback, failure classification/backoff, queue transitions/DLQ, breaker skip, internal marker forgery/loopback/depth caps, lock renewal, usage batching, handler JSON parsing. tsconfig.distillation-only.json — focused project for typecheck coverage of the new files without dragging the rest of the monorepo. Pure modules: every helper (config/scheduler/failure/selector/ internalMarker/permit/usage/lock) is side-effect-free and exercised without a real DB. The store has a complete in-memory implementation so the worker can be tested end-to-end with a fake. Storage seam: DistillationStore is the only contract the repository agent (@/memory/db) needs to satisfy. createDefaultDistillationStore() probes the anticipated paths - @/memory/db/repositories/distillation (createDistillationStore) - @/memory/db/tasks/settings (createDistillationStore) and falls back to InMemoryDistillationStore when neither is shipped. Not touched (per scope): src/memory/db/** (other agent), TencentDB-Agent- Memory snapshot, chatCore/injection, API routes, UI, MCP/CLI, docs/ licenses, src/lib/memory (existing modules), graceful shutdown. Wiring of startDistillationWorker/stopDistillationWorker into src/instrumentation-node.ts and the stop hook is intentionally left for the integration owner to land alongside the actual repository adapter. * feat(memory): four-layer API surface (L0..L3 + distillation-model + DLQ) Adds the new four-layer memory API surface over the existing /api/memory namespace, replacing the legacy engine-status / health / reindex / retrieve-preview / summarize / [id] / route routes with a strictly-typed layered API. All storage access is gated behind a dependency-injected service interface so the storage repository (TencentDB integration branch) can be slotted in without touching route handlers. New routes: - /api/memory/l0 GET list, POST import (no PUT) - /api/memory/l0/[id] GET / DELETE soft|permanent / POST op=restore - /api/memory/l0?sessionId=... POST session delete (soft|permanent) - /api/memory/l1 GET list|search, POST create - /api/memory/l1/[id] GET / PUT edit (expectedVersion) / DELETE / POST op=restore - /api/memory/l2 GET list, POST create - /api/memory/l2/[id] GET / PUT edit / DELETE / POST op=restore - /api/memory/l2/[id]/regenerate POST enqueue (409 after 15 errors) - /api/memory/l3 GET list, POST op=regenerate - /api/memory/l3/[id] GET / PUT upsert / DELETE soft|restore|permanent - /api/memory/distillation-model GET effective selector / PUT / DELETE - /api/memory/distillation-model/dlq GET list + status counts / POST op=retry Architectural pieces: - src/shared/schemas/memoryFourLayer.ts — Zod schemas (strict, 7 L1 types, optimistic version, distillation scopes, DLQ retry contract). - src/memory/api/dependencies.ts — DI registry (service, provider-model validator, audit writer, task enqueuer). Default no-op service throws 'not wired' so unconfigured deployments surface 503 instead of crashing. - src/memory/api/handlers/_lib.ts — auth subject resolution, owner isolation (self callers cannot cross owner), management override, sanitized error envelopes, audit helper. Security / hard rules: - Route handlers contain NO raw SQL (only dependency-injected service calls). - All error responses go through buildErrorBody / sanitizeErrorMessage. - No stack traces / absolute paths in response bodies. - Self API key callers cannot target a different owner via ?apiKeyId=... (403). Management callers may. - Global distillation selector requires management scope (403 otherwise). - Audit writes on every action (memory.l0.* / l1.* / l2.* / l3.* / distillation_model.* / distillation_dlq.*). Migration: - Legacy routes deleted (engine-status, health, reindex, retrieve-preview, summarize, [id]/route, root route). - Legacy integration tests removed (memory-engine-status, memory-reindex, memory-retrieve-preview, memory-route-put, memory-summarize, memory-embedding-providers, memory-route unit). - embedding-providers route KEEP-GATED: shared with the existing engine tab UI; not in the four-layer cutover scope. UI components referencing the deleted routes (EngineTab, MemoriesTab, PlaygroundTab) are out of scope per the four-layer spec; they will return 404 until the UI branch migrates. - bin/migrate-legacy-memory.ts placeholder shipped so operators can discover the migration entrypoint. Tests: - tests/unit/memory-four-layer-schemas.test.ts — 7 L1 types, optimistic version required, distillation apiKeyId conditional, DLQ retry contract. - tests/unit/memory-four-layer-deps.test.ts — DI registry swap. - tests/unit/memory-four-layer-fake-service.ts — in-memory fake service shared by route tests. - tests/integration/memory-four-layer-routes.test.ts — auth 401/403, owner isolation, validation, CRUD, L0 no PUT, optimistic conflict, regenerate 409 after 15 errors, distillation selector ladder, DLQ retry, sanitized error response, 503 when service not wired. * feat(memory): selective port of TencentDB Agent Memory pure behavior modules Adds `src/memory/tencent/**` — a snapshot-style, minimal port of pure behavior modules from TencentDB Agent Memory (MIT, Copyright (C) 2026 Tencent). Source commit: fe3230f. Ported modules (verbatim prompts, preserved regexes, preserved behavior contracts): - text/sanitize.ts — L0 capture / text-cleaning helpers (sanitizeText, stripCodeBlocks, escapeXmlTags, sanitizeJsonForParse, looksLikePromptInjection, parseExtractionScenes, ...) - text/chunking.ts — head/tail extraction chunking (applyOversizeStrategy) - text/fts.ts — FTS5 query/tokenize/BM25 helpers → REIMPLEMENTED from scratch (no copy) because the referenced upstream 'openclaw core hybrid.ts' has no verifiable license in our source tree - prompts/l1-extraction.ts — L1 scene+memory extraction prompts - prompts/l1-dedup.ts — L1 batch conflict-detection prompts - prompts/scene-extraction.ts — L2 scene-extraction prompts - prompts/persona-generation.ts — L3 persona / team-doctrine prompts - prompts/offload-prompts.ts — L1/L1.5/L2 offload prompts - parsers/json-utils.ts — tolerant JSON parsing - parsers/l1-offload-parser.ts — L1 entry parser - parsers/l15-parser.ts — L1.5 task-judgment parser - parsers/l2-offload-parser.ts — L2 MMD parser - parsers/l1-dedup-parser.ts — store|update|skip|merge parser - parsers/scene-action-parser.ts — UPDATE/MERGE/CREATE parser - recall/budget.ts — RRF k=60 + recall char budget - recall/memory-tools-guide.ts — guide + ≤3-calls/turn budget - scheduling/pipeline.ts — L1/L2/L3 warmup + threshold logic Each vendored file carries an ADAPTED FROM TencentDB Agent Memory (MIT) header pointing to the exact upstream path, source commit, and local modifications. The verbatim upstream MIT terms are at src/memory/tencent/README/LICENSE.txt. The per-file modification log is at src/memory/tencent/README/SOURCE.txt. The umbrella attribution block is at THIRD_PARTY_NOTICES.md. LICENSE gets a single trailing pointer line. Out of scope (not ported): full LLM runner, storage adapters, host hooks, MCP tools, ai-sdk / OpenClaw / @node-rs/jieba dependencies. Tests (node:test, 148 cases, 36 suites): tests/unit/memory/tencent/{sanitize,chunking,fts,parsers,actions, recall,memory-tools-guide,scheduling,prompts}.test.ts Coverage: code stripping, head/tail chunking, malformed JSON repair, parser tolerance, dedup actions, scene action parse, prompt-mode switch (chat vs code), L1/L2/L3 caps, recall budget, RRF, prompt-injection defense, scheduling thresholds. * feat(mcp,cli): four-layer memory hard cutover + owner-scoping Replace the legacy flat MCP memory surface (omniroute_memory_search / add / clear) and the legacy CLI flat verbs (search / add / clear / list / get / delete / health) with a four-layer read surface: MCP tools (all require read:memory): omniroute_memory_l0_search - vector/semantic search omniroute_memory_l1_search - full-text/FTS5 search omniroute_memory_l2_read - scene/pod read by id omniroute_memory_l3_read - current persona read omniroute_memory_list - cross-layer listing CLI verbs: memory l0 search <query> [--session --scene --limit] memory l1 search <query> [--session --scene --limit] memory l2 read <id> memory l3 read [--session] memory list [--session --scene --limit] memory settings get | set <key> <value> | reset memory distil status | retry-dlq [--yes] Owner-scoping: input schemas accept session / scene / query / limit filters only; cross-tenant apiKeyId / ownerId args are rejected by assertCallerOwner() before a single byte of data crosses the wire. The principal id is resolved via resolveMcpCallerApiKeyId() (HTTP auth context -> env fallback) and falls back to extra.authInfo.clientId through resolveCallerScopeContext(); 'anonymous' principals fail closed. Writes are not exposed yet - write:memory is reserved. Hard Rule #12: thrown errors are routed through sanitizeErrorMessage on the MCP adapter and through an inline mirror in bin/cli/commands/ memory.mjs so we never print raw stack traces / absolute source paths. Touches only the cutover scope (memoryTools.ts, server.ts minimal error sanitization, mcpScopes.ts, memory.mjs, focused tests). Storage / REST routes / UI / chatCore / instrumentation are untouched (the new /api/memory/l{0,1,2,3}/* / /api/memory/list / /api/memory/settings / /api/memory/distil/* routes are intentionally NOT added in this PR - they will be reconciled after the cherry-pick). At runtime the new tools / CLI commands fail closed with a sanitized 404/501. A2A skills already had zero imports from @/lib/memory (verified by tests/unit/mcp-memory-cutover.test.ts - the five skill entities remain, memory fields neutral). Focused tests: tests/unit/mcp-memory-cutover.test.ts (new, 12 cases) tests/unit/memory-tools.test.ts (rewritten, 9 cases) tests/unit/cli-memory-commands.test.ts (rewritten, 13 cases) tests/unit/mcp-memory-tools-strategy.test.ts (trimmed, 6 cases) tests/integration/memory-pipeline.test.ts (legacy handlers replaced with createMemory / deleteMemory / retrieveMemories - chat pipeline behavior unchanged) Suppression: cli-memory-commands.test.ts no-explicit-any count lowered 18 -> 3 (the rewrite removed the legacy test scaffolding's untyped handlers / options). Type-check (npm run typecheck:core): clean. Focused test suites (tests/unit/mcp-memory-cutover.test.ts, tests/unit/memory-tools.test.ts, tests/unit/cli-memory-commands.test.ts, tests/unit/mcp-memory-tools-strategy.test.ts, tests/unit/mcp-tool-collections-shape.test.ts): all green. Full suite is the parent agent's responsibility per the timebox. * test(sse): track Codex CLI fingerprint version via getCodexClientVersion The hardcoded 0.144.1 expectation drifted from the production constant (DEFAULT_CODEX_CLIENT_VERSION is 0.146.0). Assert against the shared getter so this test cannot silently drift again. * feat(memory): finish v4.0 four-layer hard cutover Remove the remaining v3 memory runtime (dashboard components/hooks, settings/qdrant routes, shared schemas, db/memoryVec) and land the four-layer runtime tail: standalone memory.db service + repositories + distillation migrations 006-009, distillation apply/runtime/queue and L1 scheduling seams, owner-scoped REST route adapters, MCP read tools, CLI layer commands, chatCore capture/injection wiring, and the provider-system-message hoist. Drops the Qdrant docker profile and adds src/memory to the package workspaces. * docs(memory): sync MEMORY.md, ENVIRONMENT.md, REPOSITORY_MAP and CLAUDE.md with the four-layer runtime Rewrite the MEMORY.md spec against the shipped surface (src/memory paths, 7-type L1 taxonomy, owner-scoped REST routes, five MCP read tools, real CLI commands, distillation selector/DLQ semantics). Replace the stale ENVIRONMENT.md memory/Qdrant tables with the real env surface (OMNIROUTE_MEMORY_CAPTURE/INJECTION_ENABLED + MEMORY_DISTILLATION_* + MEMORY_MIGRATIONS_DIR) and mirror it in .env.example so the env-doc-sync gate agrees. Update REPOSITORY_MAP and the CLAUDE.md project table, and retire the superseded Qdrant links in cluster-decisions. * test(memory): migrate v3-memory tests to the four-layer runtime Retire the legacy mcp-memory-tools-strategy file (strategy mapping died with src/lib/memory/settings.ts) and the v3 engine/embedding/vector/qdrant suites; migrate chat-pipeline, memory-pipeline, performance-regression, cli-memory-commands, memory-tools, route-scope and management-auth tests onto the live src/memory runtime and the owner-scoped REST surface; rewrite memory-four-layer-schemas against the shipped 7-type taxonomy; harden the worker-lifecycle lease test against parallel-run starvation; add dashboard/UI coverage for the layer tabs, distillation settings, skills tab and cutover cleanup; drop the deleted v3 include from the vitest config and cover the colocated dashboard memory tests. * chore: retrigger CI * chore: verify CI provisioning after first run * feat(memory): finish Map distillation usage + L0 telemetry Closes Map issue #2 by adding the missing owner-scoped surfaces that the four-layer hard cutover was still exposing: * GET /api/memory/distillation-model/usage — token + USD accounting for the resolved owner, with aggregate totals and a sanitized error envelope. * GET /api/memory/l0/status — masked, owner-scoped capture telemetry (success/failure counters + last-event timestamps). NEVER returns message content, prompt tokens, secrets, or correlation payloads. * DistillationSettingsTab usage card (token/USD totals, recent model+task rows, empty/error states). * L0Tab capture status card (success/failure counts, last event timestamps, masked failure category). * New migration 010_l0_capture_telemetry.sql + l0CaptureTelemetry repository that owns the SQL and the failure-category allowlist. * Retention SQL moved from integration/retentionCleaner.ts into src/memory/db/repositories/retention.ts so the persistence seam stays inside src/memory/db/ (no raw SQL outside the repository). TDD evidence: * tests/integration/memory-four-layer-routes.test.ts: - distillation usage listing is owner-scoped and returns aggregate totals (NEW) - L0 capture status is owner-scoped and exposes masked aggregate counters only (NEW) - 13/13 routes pass (12 prior + 2 new) * tests/unit/memory-four-layer-standalone-service.test.ts: - distillation usage listing remains scoped to the caller owner (NEW) - getL0CaptureStatus returns the masked counters for the caller owner only (NEW) - 9/9 service tests pass (7 prior + 2 new) * tests/unit/memory/retention.test.ts: exact-cutoff regression added (cutoff row kept, one-ms-older row removed); 7/7 pass. * tests/unit/memory-openapi-auth.test.ts: documented memory path count bumped 11 -> 13 (new usage + status endpoints). * tests/unit/DistillationSettingsTab.test.tsx: usage card rendering + totals assertion (NEW). * tests/unit/L0Tab.test.tsx: masked status card assertion (NEW). Verification: * npm run typecheck:core — clean * npm run lint — zero new errors in our diff paths * node --import tsx/esm --test on memory suites — 100% pass * npx vitest run src/app/(dashboard)/dashboard/memory — 24/24 * scripts/docs/gen-openapi-module.mjs — regenerated * scripts/check/check-docs-sync.mjs — PASS * scripts/check/check-openapi-coverage.mjs — PASS * scripts/check/check-openapi-security-tiers.mjs — PASS Boundary compliance: * No raw SQL outside src/memory/db/repositories/ * Telemetry MUST NOT block chat (failure path is fire-and-forget) * L0 message content + tokens + secrets + correlation payloads are never persisted in the telemetry table * L0 content policy unchanged — distillation inputs remain the single place where sanitization happens --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
This checkout is now an independent fork (jinnnyang/OmniRoute) whose mainline is the `less` branch. Remove the rules that only made sense while tracking diegosouzapw/OmniRoute, and keep every code-quality rule intact. - AGENTS.md: drop the "Upstream contributions", "Base-green check" and "Worktree isolation" sections; rewrite Git Workflow around `less`. - AGENTS.md: drop Hard Rules cheaper-inference#19 (worktree), cheaper-inference#21 (release-freeze) and cheaper-inference#22 (cross-session safety); renumber PII opt-in to cheaper-inference#19 and rewrite the `_tasks/` rule as cheaper-inference#20 (plain local-only gitignored dir, no remote). - AGENTS.md: #4 now bans committing broken code to `less`; cheaper-inference#16 credits jinnnyang; Testing/Review focus lose their PR-flow and VPS assumptions. - CLAUDE.md: merge the worktree/cross-session sections into one "Git" section that bans worktrees, drop the base-green section, de-hardcode paths. - GEMINI.md/CLAUDE.md: 23 Hard Rules -> 20. - GitNexus detect_changes base_ref: release/v3.8.50 -> less.
… Rule cheaper-inference#20 (cheaper-inference#5159) Integrated into release/v3.8.39. PII opt-in regression guard + Hard Rule cheaper-inference#20; rebased to strip base-drift (+81/-1). 5/5 guard tests pass; flip-proof verified.
…dacted
Turns were read before the post-call PII masker and the streaming PII transform,
so /v1/me/sessions/{id}/messages could serve text the client received redacted.
saveRequestUsage now runs the same passes over the stored turn:
- user text: request PII masking (PII_REDACTION_ENABLED, processPII), which the
upstream saw, and credential redaction;
- assistant text: response PII sanitization (PII_RESPONSE_SANITIZATION and its
mode, sanitizePII), and credential redaction.
All of them stay opt-in (Hard Rule cheaper-inference#20): with every flag off the turn is stored
verbatim. The credential switch is now one exported helper shared with the masker.
* chore(release): open v3.8.39 development cycle * docs(changelog): backfill 5 v3.8.38 bullets merged after release finalize These PRs squash-merged into release/v3.8.38 between the CHANGELOG finalize (4abac1642) and the merge-to-main (6d4b6e0ba), so they shipped in the v3.8.38 tag but had no bullet: - feat(compression): Ionizer engine (lossy JSON-array sampling + CCR) (cheaper-inference#5148) - fix(sse): preserve non-stream reasoning fields (cheaper-inference#5155, @rdself) - fix(i18n): add missing English UI labels (cheaper-inference#5153, @rdself) - test(combo): gated live smoke (cheaper-inference#5151) + release-expectations refresh (cheaper-inference#5150, @KooshaPari) (cheaper-inference#5129 exact-host Anthropic baseUrl is already covered by the cheaper-inference#5130 bullet — same CodeQL cheaper-inference#674.) Synced 41 i18n CHANGELOG mirrors. * feat(compression): TOON best-of-N candidate encoder + encoder A/B table (cheaper-inference#5163) Integrated into release/v3.8.39. TOON best-of-N candidate encoder (GCF default, fail-open). 17/17 unit tests pass on merge result; CI reds were base-stale + Quality Ratchet DRIFT. * fix(zenmux): normalize vendor-prefixed GLM system roles (cheaper-inference#5158) Integrated into release/v3.8.39. ZenMux vendor-prefixed GLM system-role normalization; 12/12 role-normalizer tests pass on merge result. CI reds base-stale. * [codex] fix xAI OAuth test and reasoning effort (cheaper-inference#5157) Integrated into release/v3.8.39. xAI reasoning-effort normalization (max/xhigh→high) + OAuth test config; 46/46 xai-translator tests pass on merge result. CI reds base-stale. * docs(i18n): add Traditional Chinese (zh-TW) README and update zh-CN to latest (cheaper-inference#5162) Integrated into release/v3.8.39. Traditional Chinese (zh-TW) README + zh-CN refresh; docs-only. * test(security): guard PII redaction stays opt-in (default off) + Hard Rule cheaper-inference#20 (cheaper-inference#5159) Integrated into release/v3.8.39. PII opt-in regression guard + Hard Rule cheaper-inference#20; rebased to strip base-drift (+81/-1). 5/5 guard tests pass; flip-proof verified. * test(combo): deterministic context-relay universal-handoff coverage (closes phase-2 TODO) (cheaper-inference#5168) Integrated into release/v3.8.39. Deterministic context-relay universal-handoff coverage (3 tests); 3/3 pass on merge result. * docs(i18n): full sync zh-TW and zh-CN README with canonical English v3.8.39 (cheaper-inference#5171) Integrated into release/v3.8.39. Full zh-TW docs tree + zh-CN sync with canonical English v3.8.39; docs-only. * fix(serve): honour HOSTNAME from .env instead of hardcoding 0.0.0.0 (cheaper-inference#5134) (cheaper-inference#5170) Integrated into release/v3.8.39. HOSTNAME env override in serve (cheaper-inference#5134) + regression test (4/4, TDD flip-proof verified). * fix(sse): resolve nameless deepseek-web tool blocks via parameter-schema match (cheaper-inference#5154) (cheaper-inference#5173) Integrated into release/v3.8.39. Schema-based nameless deepseek-web tool-block resolution (cheaper-inference#5154); 6/6 tests pass on merge result (incl. ambiguous/no-match negatives + named-tag no-regression). * fix(sse): normalize array user content for Command Code to avoid upstream 400 (cheaper-inference#5166) (cheaper-inference#5174) Integrated into release/v3.8.39. Normalize array user content for Command Code (cheaper-inference#5166, user-array/400 symptom); 4/4 tests pass on merge result. * fix(sse): defer </think> close so it never leaks before tool_calls (cheaper-inference#5123) (cheaper-inference#5175) Integrated into release/v3.8.39. Defer </think> close so it never leaks before tool_calls (cheaper-inference#5123); 4/4 tests pass (incl. cheaper-inference#4633 no-regression). CHANGELOG synced to keep all 3 v3.8.39 fixes. * fix(dashboard): use amber for home update-step warning icon (cheaper-inference#5176) Integrated into release/v3.8.39. Amber for home update-step warning icon; 1/1 UI test. * fix(api): LAN/Tailscale dashboard — host-aware CSP + GET-exempt version route + combo field errors (cheaper-inference#5083) (cheaper-inference#5177) Integrated into release/v3.8.39. Host-aware CSP (ReDoS/injection-safe host validation) + GET-exempt /api/system/version (POST/spawn stays LOCAL_ONLY, exact-match safe-methods-only) + COMBO_002 firstField. 44/44 tests + route-guard membership gate green. CHANGELOG synced to keep all 4 v3.8.39 fixes. * fix(api): replace cheaper-inference#5083 global middleware CSP with declarative ws: scheme (cheaper-inference#5083) Follow-up to PR cheaper-inference#5177 (merged): that version implemented the LAN-CSP fix (Bug 1) with a new global `src/middleware.ts` + `src/server/csp.ts`, which contradicts the project's documented architecture — 'No global Next.js middleware — interception is route-specific' (CLAUDE.md / AGENTS.md) — and was merged unverified (middleware vs next.config header precedence was never confirmed in a real build). This replaces that approach with the minimal, declarative equivalent: • next.config.mjs: connect-src now permits the bare `ws:` scheme (symmetric with the bare `wss:` already allowed) so the dashboard can reach its own Live WS server from a LAN/Tailscale host. No middleware. • Removes src/middleware.ts, src/server/csp.ts, and tests/unit/csp-host-aware.test.ts. • Adds tests/unit/csp-lan-ws-5083.test.ts (incl. a guard asserting src/middleware.ts does NOT exist, so the global-middleware approach cannot silently return). Bugs 2 (GET-exempt /api/system/version) and 3 (COMBO_002 field surfacing) from cheaper-inference#5177 are unaffected and remain in place. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> * test(combo): end-to-end quota-share DRR routing-decision coverage (matrix parity) (cheaper-inference#5179) Integrated into release/v3.8.39. Quota-share DRR routing-decision coverage (matrix parity); 2/2 pass on merge result. * feat(agent-bridge): graceful cert-install fallback with manual guide for containers (cheaper-inference#4546) (cheaper-inference#5178) Integrated into release/v3.8.39. Agent-bridge graceful cert-install fallback + manual guide (cheaper-inference#4546); 6/6 tests pass on merge result. * fix(antigravity): family-scoped quota lockout (gemini/claude buckets) (cheaper-inference#5180) Integrated into release/v3.8.39 — family-scoped antigravity quota lockout. Rebased from v3.8.37 + validated (vitest 5/5, typecheck clean, full combo-matrix green, model-lockout 99/0). Same-model cross-account retry (chat.ts) deferred pending live antigravity VPS validation. * fix(cli): force NODE_ENV to match dev/start run mode in custom Next server (cheaper-inference#5189) Integrated into release/v3.8.39. Force NODE_ENV to match dev/start run mode in custom Next server; 2/2 source-scan+ordering tests pass on merge result. * feat(compression): CCR ranged/grep/stats retrieval (ReDoS-safe, backward-compat) (cheaper-inference#5187) Integrated into release/v3.8.39. CCR ranged/grep/stats retrieval (safe-regex ReDoS guard + length/match caps); 17/17 tests pass on merge result. * docs(combo): sync all combo/routing-strategy docs to current state + document test coverage (cheaper-inference#5185) Integrated into release/v3.8.39. Combo/routing-strategy docs sync; docs-only. * fix(mcp): return 404 (not 400) for unknown Streamable HTTP session id (cheaper-inference#5169) (cheaper-inference#5191) * fix(api): respect blocked Auto (Zero-Config) provider in /v1/models catalog (cheaper-inference#5192) (cheaper-inference#5194) * test(combo): deterministic context-relay codex quota-handoff coverage (closes last gap) (cheaper-inference#5195) * test(ci): wire antigravity-quota-family under test:vitest (fix test-discovery orphan) (cheaper-inference#5196) * fix(oauth): antigravity login no longer hangs — fire-and-forget onboarding + bounded post-exchange (cheaper-inference#5193) Antigravity OAuth hang fix (no-PKCE/no-openid + bounded post-exchange + exchange-500 fix). Includes cheaper-inference#5200 (Koosha) revert + owner rebaseline to keep documented comments. Integrated into release/v3.8.39. * feat(oauth): remote Antigravity login via local helper + paste-credentials (cheaper-inference#5203) Remote Antigravity login: local helper (omniroute login antigravity) + paste-credentials. Integrated into release/v3.8.39. * fix(translator): accept Claude Messages shape in non-stream malformed-200 guard (cheaper-inference#5156) Integrated into release/v3.8.39 * fix(cli): default dev bundler to Turbopack (16.2.x panic no longer reproduces) (cheaper-inference#5206) Integrated into release/v3.8.39 * fix(cli): auto-calibrate server V8 heap from physical RAM (cheaper-inference#5172) (cheaper-inference#5213) The server was spawned with a fixed --max-old-space-size=512 (omniroute serve) or no heap flag at all (Electron), so RAM-rich boxes still OOM-crashed under load (Ineffective mark-compacts near heap limit ~500MB) with many providers/ accounts and large model catalogs. New calibrateHeapFallbackMb(os.totalmem()) defaults the heap to ~35% of RAM clamped [512,4096], wired into serve.mjs and electron/main.js. Explicit OMNIROUTE_MEMORY_MB still wins (cheaper-inference#2939 unchanged). Also addresses cheaper-inference#5160 (same OOM root); cheaper-inference#5152 (docker) benefits via the same knob. Closes cheaper-inference#5172 * fix(proxy): coalesce fast-fail health probes (cheaper-inference#5208) Integrated into release/v3.8.39 * fix(proxy): close dispatchers when clearing cache (cheaper-inference#5202) Integrated into release/v3.8.39 * fix(cli): raise dev server Node heap limit to 8GB to prevent OOM (cheaper-inference#5198) Integrated into release/v3.8.39 * fix(auth): allow synthetic no-auth fallback for mimocode (cheaper-inference#5205) Integrated into release/v3.8.39 * fix(oauth): preserve Antigravity refresh_token on empty/omitted upstream response (cheaper-inference#3850) (cheaper-inference#5214) Google's OAuth refresh tokens are non-rotating: the refresh response usually omits refresh_token and occasionally returns it as an empty string. The Antigravity executor used `typeof tokens.refresh_token === "string" ? ... ` which accepts "" (typeof "" === "string") and overwrote the stored token with empty, nulling it on first refresh. Now treats non-string OR empty as absent and preserves credentials.refreshToken, matching refreshGoogleToken semantics. Closes cheaper-inference#3850 * fix(responses): normalize non-array input (cheaper-inference#5204) Integrated into release/v3.8.39 * fix(stream): normalize safety finish reasons via shared helper (cheaper-inference#5197) Integrated into release/v3.8.39 * fix(request-logger): never render negative '(-100%)' compression badge (cheaper-inference#5201) Integrated into release/v3.8.39 * fix(combo): reject empty responses api output (cheaper-inference#5207) Integrated into release/v3.8.39 — combo failover now rejects empty Responses API output (validateQuality). Baseline rebaseline dropped (main-measured drift; maintainer rebaselines at release). * fix(pwa): prefer cached navigation before offline page (cheaper-inference#5209) Integrated into release/v3.8.39 — PWA service worker prefers cached navigation before offline page (cheaper-inference#5165). * chore(release): v3.8.39 — 2026-06-28 * chore(release): rebaseline openapi+i18n coverage ratchet drift for v3.8.39 --------- Co-authored-by: Arthur Bodera <abodera@gmail.com> Co-authored-by: Nguyen Minh <lop123thcs@gmail.com> Co-authored-by: lunkerchen <labanchen@gmail.com> Co-authored-by: Ankit <177378174+anki1kr@users.noreply.github.com> Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> Co-authored-by: Ardem2025 <ardemb22@gmail.com> Co-authored-by: backryun <bakryun0718@proton.me> Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com> Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Co-authored-by: Wilson <pedbookmed@gmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
…peline (cheaper-inference#7339) (cheaper-inference#7736) Phases 3-4 of cheaper-inference#3384 (Phases 1-2 shipped DB schema + resolveInterceptSearch in release/v3.8.47). Adds resolveInterceptFetch(provider, model) as a structural twin of resolveInterceptSearch, and open-sse/services/webFetchInterception.ts (mirroring webSearchFallback.ts) to rewrite a provider-native web_fetch tool declaration into a synthetic omniroute_web_fetch function tool. The synthetic tool call is dispatched through the existing handleToolCallExecution path (same as omniroute_web_search) to a new web_fetch builtin skill handler that resolves credentials and calls handleWebFetch() against /v1/web/fetch. Strictly opt-in: with no interceptFetch DB row configured (the default), the outgoing request body is byte-identical to pre-change behavior — no heuristic default bypass like the interceptSearch sibling, to guarantee zero overhead when disabled (Hard Rule cheaper-inference#20). Also ships the dashboard toggle (owner decision, overriding the plan's backend-only recommendation): ProviderInterceptionSection.tsx on the provider detail page, backed by GET/PUT/DELETE /api/providers/[id]/interception-rules, covering both interceptSearch and interceptFetch from one control. chatCore.ts touch is minimal (frozen file): one resolver call + one prepareWebFetchFallbackBody call mirroring the existing interceptSearch block, plus threading provider/model into the existing handleToolCallExecution call.
sanitizePayloadPII deep-copied every log payload and resolved the PII_RESPONSE_SANITIZATION flag from SQLite once per string, even though the flag is off by default (Hard Rule cheaper-inference#20). The live pending-request preview runs protectPayloadForLog on the full request body at every stage update, so in a production inspector profile (4 x 90 s at peak) these per-string flag lookups were ~32% of the router's main-thread busy time, blocking the event loop for 0.3-2 s on 300-800 KB requests. Resolve the flag once per payload and return the payload untouched when it is off; redactPayload still copies it and describes binary values, so the logged output is unchanged. After deploying this, the same profile showed protectPayloadForLog at 7% of busy time (was 43%) and time spent in event-loop stalls fell from 42% to 11% at matched load.
Release v0.2.0
Highlights
Stats