chore(release): prepare v3.4.2 integration branch
This commit is contained in:
@@ -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
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute-desktop",
|
||||
"version": "3.4.1",
|
||||
"version": "3.4.2",
|
||||
"description": "OmniRoute Desktop Application",
|
||||
"main": "main.js",
|
||||
"author": {
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
Generated
+4
-51
@@ -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
@@ -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": {
|
||||
|
||||
Vendored
+1
-1
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user