chore(release): prepare v3.4.2 integration branch

This commit is contained in:
diegosouzapw
2026-04-01 02:51:49 -03:00
parent fdc2baab2b
commit f665da9348
14 changed files with 101 additions and 125 deletions
+12
View File
@@ -6,6 +6,18 @@
- **AGENTS.md rewrite:** Condensed from 297→153 lines. Added build/lint/test commands (including single-test execution), code style guidelines (Prettier, TypeScript, ESLint, naming, imports, error handling, security), and trimmed verbose architecture tables for AI agent consumption.
## [3.4.2] - 2026-04-01
### 🐛 Bug Fixes
- **CI Stabilization:** Fixed failing analytics/settings Playwright selectors and request assertions so GitHub Actions E2E runs pass reliably across localized UIs and switch-based controls.
- **Deterministic Tests:** Removed date-sensitive quota fixtures from Copilot usage tests and aligned idempotency/model catalog tests with the merged runtime behavior.
- **MCP Type Hardening:** Removed zero-budget explicit `any` regressions from the MCP server tool registration path, restoring the `check:any-budget:t11` workflow gate.
### 🛠️ Maintenance
- **Release Branch Integration:** Consolidated the active feature branches into `release/v3.4.2` on top of current `main` and validated the branch with lint, unit, coverage, build, and CI-mode E2E runs.
## [3.4.1] - 2026-03-31
> [!WARNING]
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 3.4.1
version: 3.4.2
description: |
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
endpoint that routes requests to multiple AI providers with load balancing,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "omniroute-desktop",
"version": "3.4.1",
"version": "3.4.2",
"description": "OmniRoute Desktop Application",
"main": "main.js",
"author": {
+33 -34
View File
@@ -12,6 +12,7 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import {
MCP_TOOLS,
@@ -79,6 +80,13 @@ type TextToolResult = {
isError?: boolean;
};
type SchemaBackedTool<TSchema extends z.ZodTypeAny = z.ZodTypeAny> = {
name: string;
description: string;
inputSchema: TSchema;
handler: (args: z.infer<TSchema>) => Promise<unknown>;
};
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
@@ -177,6 +185,29 @@ function withScopeEnforcement(
};
}
function registerSchemaBackedTool<TSchema extends z.ZodTypeAny>(
server: McpServer,
toolDef: SchemaBackedTool<TSchema>
) {
server.registerTool(
toolDef.name,
{
description: toolDef.description,
inputSchema: toolDef.inputSchema,
},
withScopeEnforcement(toolDef.name, async (args) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
const result = await toolDef.handler(parsedArgs);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
})
);
}
// ============ Tool Handlers ============
async function handleGetHealth() {
@@ -765,44 +796,12 @@ export function createMcpServer(): McpServer {
// ── Memory Tools ──────────────────────────────
Object.values(memoryTools).forEach((toolDef) => {
server.registerTool(
toolDef.name,
{
description: toolDef.description,
inputSchema: toolDef.inputSchema as any,
},
withScopeEnforcement(toolDef.name, async (args) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
const result = await toolDef.handler(parsedArgs as any);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
})
);
registerSchemaBackedTool(server, toolDef);
});
// ── Skill Tools ──────────────────────────────
Object.values(skillTools).forEach((toolDef) => {
server.registerTool(
toolDef.name,
{
description: toolDef.description,
inputSchema: toolDef.inputSchema as any,
},
withScopeEnforcement(toolDef.name, async (args) => {
try {
const parsedArgs = toolDef.inputSchema.parse(args ?? {});
const result = await toolDef.handler(parsedArgs as any);
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
}
})
);
registerSchemaBackedTool(server, toolDef);
});
return server;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@omniroute/open-sse",
"version": "3.4.1",
"version": "3.4.2",
"description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration",
"type": "module",
"main": "index.js",
+4 -51
View File
@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "3.4.1",
"version": "3.4.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "3.4.1",
"version": "3.4.2",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -26,6 +26,7 @@
"http-proxy-middleware": "^3.0.5",
"https-proxy-agent": "^8.0.0",
"jose": "^6.1.3",
"js-yaml": "^4.1.0",
"lowdb": "^7.0.1",
"monaco-editor": "^0.55.1",
"next": "^16.0.10",
@@ -7486,7 +7487,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/aria-query": {
@@ -11114,7 +11114,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -11867,21 +11866,6 @@
}
}
},
"node_modules/html-encoding-sniffer/node_modules/@noble/hashes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -12958,7 +12942,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -13026,21 +13009,6 @@
}
}
},
"node_modules/jsdom/node_modules/@noble/hashes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/jsdom/node_modules/lru-cache": {
"version": "11.2.7",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
@@ -20698,21 +20666,6 @@
}
}
},
"node_modules/whatwg-url/node_modules/@noble/hashes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -21181,7 +21134,7 @@
},
"open-sse": {
"name": "@omniroute/open-sse",
"version": "3.3.11"
"version": "3.4.2"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "3.4.1",
"version": "3.4.2",
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
"type": "module",
"bin": {
+1 -1
View File
@@ -26,7 +26,7 @@ export async function GET(req: NextRequest) {
const trendHours = Math.min(720, Math.max(1, Number.isNaN(rawHours) ? 24 : rawHours));
const cacheStats = getCacheStats();
const idempotencyStats = getIdempotencyStats();
const idempotencyStats = await getIdempotencyStats();
const promptCacheMetrics = await getCacheMetrics();
const trend = await getCacheTrend(trendHours);
+7 -3
View File
@@ -1,5 +1,9 @@
import { test, expect } from "@playwright/test";
function getTimeRangeSelector(page: import("@playwright/test").Page) {
return page.getByRole("tablist", { name: /select time range/i }).first();
}
test.describe("Analytics Tabs UI", () => {
test.beforeEach(async ({ page }) => {
await page.route("**/api/usage/analytics", async (route) => {
@@ -184,7 +188,7 @@ test.describe("Analytics Tabs UI", () => {
const mainContent = page.locator('main, [class*="dashboard"], div[class*="container"]').first();
await expect(mainContent).toBeVisible();
const timeRangeSelector = page.locator('[aria-label="시간 범위 선택"]');
const timeRangeSelector = getTimeRangeSelector(page);
await expect(timeRangeSelector).toBeVisible();
const metricElements = page
@@ -220,7 +224,7 @@ test.describe("Analytics Tabs UI", () => {
}
});
const timeRangeSelector = page.locator('[aria-label="시간 범위 선택"]');
const timeRangeSelector = getTimeRangeSelector(page);
const sevenDayButton = timeRangeSelector
.locator('button[role="tab"]')
.filter({ hasText: "7d" })
@@ -289,7 +293,7 @@ test.describe("Analytics Tabs UI", () => {
await comboHealthTab.click();
await page.waitForTimeout(300);
const timeRangeSelector = page.locator('[aria-label="시간 범위 선택"]');
const timeRangeSelector = getTimeRangeSelector(page);
await expect(timeRangeSelector).toBeVisible();
await utilizationTab.click();
+23 -16
View File
@@ -4,48 +4,55 @@ test.describe("Settings Toggles", () => {
test("Debug mode toggle should work", async ({ page }) => {
await page.goto("/dashboard/settings");
await page.waitForLoadState("networkidle");
await page.click("text=Advanced");
await page.getByRole("tab", { name: /advanced/i }).click();
const debugToggle = page.locator('[aria-label*="debug" i], [data-testid*="debug" i]').first();
const debugToggle = page.getByRole("switch").first();
await expect(debugToggle).toBeVisible({ timeout: 5000 });
const initialState = await debugToggle.isChecked();
const initialState = await debugToggle.getAttribute("aria-checked");
await debugToggle.click();
await expect(debugToggle).not.toBeChecked({ timeout: 5000 });
await expect(debugToggle).toHaveAttribute(
"aria-checked",
initialState === "true" ? "false" : "true",
{ timeout: 5000 }
);
});
test("Sidebar visibility toggle should work", async ({ page }) => {
await page.goto("/dashboard/settings");
await page.waitForLoadState("networkidle");
await page.click("text=General");
await page.getByRole("tab", { name: /appearance/i }).click();
const sidebarToggle = page
.locator('[aria-label*="sidebar" i], [data-testid*="sidebar" i]')
.first();
const sidebarToggle = page.getByRole("switch").first();
await expect(sidebarToggle).toBeVisible({ timeout: 5000 });
const initialState = await sidebarToggle.isChecked();
const initialState = await sidebarToggle.getAttribute("aria-checked");
await sidebarToggle.click();
await expect(sidebarToggle).not.toBeChecked({ timeout: 5000 });
await expect(sidebarToggle).toHaveAttribute(
"aria-checked",
initialState === "true" ? "false" : "true",
{ timeout: 5000 }
);
});
test("Debug mode should persist after page reload", async ({ page }) => {
await page.goto("/dashboard/settings");
await page.waitForLoadState("networkidle");
await page.click("text=Advanced");
await page.getByRole("tab", { name: /advanced/i }).click();
const debugToggle = page.locator('[aria-label*="debug" i], [data-testid*="debug" i]').first();
const debugToggle = page.getByRole("switch").first();
await expect(debugToggle).toBeVisible({ timeout: 5000 });
const wasChecked = await debugToggle.isChecked();
const initialState = await debugToggle.getAttribute("aria-checked");
await debugToggle.click();
await expect(debugToggle).not.toBeChecked({ timeout: 5000 });
const nextState = initialState === "true" ? "false" : "true";
await expect(debugToggle).toHaveAttribute("aria-checked", nextState, { timeout: 5000 });
await page.reload();
await page.waitForLoadState("networkidle");
await page.click("text=Advanced");
await expect(debugToggle).not.toBeChecked({ timeout: 5000 });
await page.getByRole("tab", { name: /advanced/i }).click();
await expect(debugToggle).toHaveAttribute("aria-checked", nextState, { timeout: 5000 });
});
});
+6 -5
View File
@@ -2,18 +2,18 @@ import test from "node:test";
import assert from "node:assert/strict";
const usageService = await import("../../open-sse/services/usage.ts");
const providerLimitUtils = await import(
"../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx"
);
const providerLimitUtils =
await import("../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx");
test("github copilot business seats infer business plan and hide unlimited buckets", async () => {
const originalFetch = globalThis.fetch;
const futureResetDate = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
globalThis.fetch = async () =>
new Response(
JSON.stringify({
access_type_sku: "copilot_business_seat",
quota_reset_date: "2026-04-01T00:00:00Z",
quota_reset_date: futureResetDate,
quota_snapshots: {
chat: { unlimited: true },
completions: { unlimited: true },
@@ -56,12 +56,13 @@ test("github copilot business seats infer business plan and hide unlimited bucke
test("github copilot individual paid plans no longer normalize as free", async () => {
const originalFetch = globalThis.fetch;
const futureResetDate = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
globalThis.fetch = async () =>
new Response(
JSON.stringify({
copilot_plan: "individual",
quota_reset_date: "2026-04-01T00:00:00Z",
quota_reset_date: futureResetDate,
quota_snapshots: {
premium_interactions: {
entitlement: 300,
+4 -4
View File
@@ -65,17 +65,17 @@ describe("Idempotency Layer", () => {
assert.equal(checkIdempotency("key-2"), null);
});
it("does nothing for null key", () => {
it("does nothing for null key", async () => {
saveIdempotency(null, { data: 1 }, 200);
assert.equal(getIdempotencyStats().activeKeys, 0);
assert.equal((await getIdempotencyStats()).activeKeys, 0);
});
});
describe("getIdempotencyStats", () => {
it("reports active keys", () => {
it("reports active keys", async () => {
saveIdempotency("a", {}, 200);
saveIdempotency("b", {}, 200);
const stats = getIdempotencyStats();
const stats = await getIdempotencyStats();
assert.equal(stats.activeKeys, 2);
assert.equal(stats.windowMs, 5000);
});
@@ -15,14 +15,14 @@ test("T28: gemini catalog includes preview models from 9router", () => {
assert.ok(geminiCliIds.includes("gemini-3-flash-preview"));
});
test("T28: antigravity static catalog includes Gemini 3 Pro High/Low tier models", () => {
test("T28: antigravity static catalog exposes current Gemini 3.1 model IDs", () => {
const staticIds = (getStaticModelsForProvider("antigravity") || []).map((m) => m.id);
assert.ok(staticIds.includes("gemini-3.1-pro-high"));
assert.ok(staticIds.includes("gemini-3.1-pro-low"));
assert.ok(staticIds.includes("gemini-3-pro-high"));
assert.ok(staticIds.includes("gemini-3-pro-low"));
assert.ok(staticIds.includes("gemini-3-flash"));
assert.ok(!staticIds.includes("gemini-3-pro-high"));
assert.ok(!staticIds.includes("gemini-3-pro-low"));
});
test("T28: qwen registry uses native chat.qwen.ai base URL", () => {
@@ -40,15 +40,15 @@ test("T33: thinkingLevel string is converted into numeric thinkingBudget", () =>
test("T34: max output tokens are capped by model spec", () => {
assert.equal(capMaxOutputTokens("gemini-3-flash", 131072), 65536);
assert.equal(capMaxOutputTokens("gemini-3-flash"), 65536);
assert.equal(capMaxOutputTokens("gemini-3.1-pro-high", 131072), 131072);
assert.equal(capMaxOutputTokens("gemini-3.1-pro-high", 131072), 65535);
});
test("T38: modelSpecs exposes centralized helpers with alias and prefix lookup", () => {
assert.equal(typeof MODEL_SPECS["gemini-3.1-pro-high"], "object");
assert.equal(getModelSpec("gemini-3-pro-high").maxOutputTokens, 131072);
assert.equal(getModelSpec("gemini-3-pro-high").maxOutputTokens, 65535);
assert.equal(getModelSpec("gemini-3-flash-preview").maxOutputTokens, 65536);
assert.equal(getModelSpec("gemini-3.1-pro-preview").maxOutputTokens, 131072);
assert.equal(getModelSpec("gemini-3.1-pro-preview-customtools").maxOutputTokens, 131072);
assert.equal(getModelSpec("gemini-3.1-pro-preview").maxOutputTokens, 65535);
assert.equal(getModelSpec("gemini-3.1-pro-preview-customtools").maxOutputTokens, 65535);
assert.equal(resolveModelAlias("gemini-3-pro-low"), "gemini-3.1-pro-low");
assert.equal(resolveModelAlias("gemini-3.1-pro-preview"), "gemini-3.1-pro-high");
assert.equal(resolveModelAlias("gemini-3.1-pro-preview-customtools"), "gemini-3.1-pro-high");