diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f6fdc96d0..80b3a71240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,6 @@ ### 🐛 Bug Fixes -- **fix(providers):** resolve ChatGPT Web authentication failure by aligning TLS fingerprint User-Agent strings (fixes #1925) -- **fix(infrastructure):** move `wreq-js` to optionalDependencies to unblock ARM64/Android Termux installations and add Node 25/26 to secure runtime policy (fixes #1924) - **fix(providers):** allow local OpenAI-compatible endpoints (like Ollama) to be added without an API key (fixes #1893) - **fix(providers):** bypass AgentRouter unauthorized_client_error by spoofing Claude CLI headers via Anthropic endpoints (fixes #1921) - **fix(copilot):** emit compatible reasoning text deltas (#1919 — thanks @ivan-mezentsev) @@ -44,8 +42,6 @@ - **fix(migrations):** duplicate-column no-op fix (#1886 — thanks @smartenok-ops) - **fix(auth):** per-connection OAuth refresh mutex (#1885 — thanks @smartenok-ops) - **fix(auth):** require dashboard management auth for compression preview -- **fix(mitm):** add root user and system-level certificate authority support for MITM proxy on Linux/Android (#1948) -- **fix(registry):** remove deprecated GPT-5.5 pricing tiers, GPT-5.4-nano, and update Grok vision model catalog (#1949) ### 📝 Documentation diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 7a2faa2c27..34d12994ce 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -934,11 +934,9 @@ function isCopilotClient( if (isMatch(userAgent)) return true; if (headers instanceof Headers) { - let found = false; - headers.forEach((value, key) => { - if (isMatch(key) || isMatch(value)) found = true; - }); - if (found) return true; + for (const [key, value] of headers) { + if (isMatch(key) || isMatch(value)) return true; + } } else if (headers && typeof headers === "object") { for (const [key, value] of Object.entries(headers)) { if (isMatch(key) || isMatch(value)) return true; diff --git a/open-sse/mcp-server/descriptionCompressor.ts b/open-sse/mcp-server/descriptionCompressor.ts index e83ecde0c2..e69a86ee4d 100644 --- a/open-sse/mcp-server/descriptionCompressor.ts +++ b/open-sse/mcp-server/descriptionCompressor.ts @@ -67,7 +67,7 @@ export function compressMcpDescription(description: string): DescriptionCompress .replace(/[ \t]{2,}/g, " ") .replace(/[ \t]+([,.;:!?])/g, "$1") .replace(/\n{3,}/g, "\n\n") - .replace(/(^|[.!?]\s+|^\s+)([a-z])/gm, (_match, prefix: string, char: string) => { + .replace(/(^|[.!?][ \t]+|\n+[ \t]*)([a-z])/g, (_match, prefix: string, char: string) => { return `${prefix}${char.toUpperCase()}`; }) .trim(); diff --git a/open-sse/services/compression/caveman.ts b/open-sse/services/compression/caveman.ts index d88182348a..476be8d41e 100644 --- a/open-sse/services/compression/caveman.ts +++ b/open-sse/services/compression/caveman.ts @@ -216,9 +216,12 @@ function cleanupArtifacts(text: string): string { } function recapitalizeSentences(text: string): string { - return text.replace(/(^|[.!?]\s+|^\s+)([a-z])/gm, (_match, prefix: string, char: string) => { - return `${prefix}${char.toUpperCase()}`; - }); + return text.replace( + /(^|[.!?][ \t]+|\n+[ \t]*)([a-z])/g, + (_match, prefix: string, char: string) => { + return `${prefix}${char.toUpperCase()}`; + } + ); } function createCavemanStats( diff --git a/open-sse/services/compression/validation.ts b/open-sse/services/compression/validation.ts index 8a39537665..7f2ae31115 100644 --- a/open-sse/services/compression/validation.ts +++ b/open-sse/services/compression/validation.ts @@ -67,13 +67,13 @@ export function validateCompression(original: string, compressed: string): Valid requireExactPresence("heading", collectMatches(original, /^#{1,6}\s+.+$/gm), compressed, errors); requireExactPresence( "table row", - collectMatches(original, /^[ \t]*\|(?:[^|\n]*\|)+[ \t]*$/gm), + collectMatches(original, /^[ \t]*\|(?:[^|\n]{0,1000}\|){1,100}[ \t]*$/gm), compressed, errors ); requireExactPresence( "math block", - collectMatches(original, /\$\$[^$]*(?:\$(?!\$)[^$]*)*\$\$/g), + collectMatches(original, /\$\$[\s\S]{0,10000}?\$\$/g), compressed, errors ); @@ -85,10 +85,7 @@ export function validateCompression(original: string, compressed: string): Valid ); requireExactPresence( "LaTeX block", - collectMatches( - original, - /\\begin\{[A-Za-z*]{1,50}\}(?:(?!\\end\{)[^])*\\end\{[A-Za-z*]{1,50}\}/g - ), + collectMatches(original, /\\begin\{[A-Za-z*]{1,50}\}[\s\S]{0,10000}?\\end\{[A-Za-z*]{1,50}\}/g), compressed, errors ); diff --git a/package-lock.json b/package-lock.json index 810eda63e9..8753eccd38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,29 +13,29 @@ "open-sse" ], "dependencies": { - "@lobehub/icons": "^5.6.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@lobehub/icons": "^5.5.4", + "@modelcontextprotocol/sdk": "^1.27.1", "@monaco-editor/react": "^4.7.0", "@ngrok/ngrok": "^1.7.0", "@swc/helpers": "0.5.21", - "axios": "^1.16.0", + "axios": "^1.15.0", "bcryptjs": "^3.0.3", - "better-sqlite3": "^12.9.0", + "better-sqlite3": "^12.6.2", "bottleneck": "^2.19.5", "express": "^5.2.1", - "fetch-socks": "^1.3.3", + "fetch-socks": "^1.3.2", "http-proxy-middleware": "^3.0.5", "https-proxy-agent": "^9.0.0", - "jose": "^6.2.3", - "js-yaml": "^4.1.1", + "jose": "^6.1.3", + "js-yaml": "^4.1.0", "jsonc-parser": "^3.3.1", "lowdb": "^7.0.1", "monaco-editor": "^0.55.1", - "next": "^16.2.4", + "next": "^16.2.3", "next-intl": "^4.11.0", "node-machine-id": "^1.1.12", "open": "^11.0.0", - "ora": "^9.4.0", + "ora": "^9.1.0", "pino": "^10.3.1", "pino-abstract-transport": "^3.0.0", "pino-pretty": "^13.1.3", @@ -43,56 +43,56 @@ "react-dom": "19.2.5", "react-is": "^19.2.5", "react-markdown": "^10.1.0", - "recharts": "^3.8.1", + "recharts": "^3.7.0", "selfsigned": "^5.5.0", "tls-client-node": "^0.1.13", "tsx": "^4.21.0", - "undici": "^8.2.0", + "undici": "^8.1.0", "uuid": "^14.0.0", + "wreq-js": "^2.0.1", "xxhash-wasm": "^1.1.0", "yazl": "^3.3.1", - "zod": "^4.4.3", - "zustand": "^5.0.12" + "zod": "^4.3.6", + "zustand": "^5.0.10" }, "bin": { "omniroute": "bin/omniroute.mjs", "omniroute-reset-password": "bin/reset-password.mjs" }, "devDependencies": { - "@playwright/test": "^1.59.1", - "@tailwindcss/postcss": "^4.2.4", + "@playwright/test": "^1.58.2", + "@tailwindcss/postcss": "^4.1.18", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/bcryptjs": "^3.0.0", "@types/better-sqlite3": "^7.6.13", - "@types/keytar": "^4.4.2", - "@types/node": "^25.6.0", + "@types/keytar": "^4.4.0", + "@types/node": "^25.2.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "c8": "^11.0.0", "concurrently": "^9.2.1", "cross-env": "^10.1.0", - "eslint": "^9.39.4", + "eslint": "^9.39.2", "eslint-config-next": "16.2.4", "husky": "^9.1.7", - "jsdom": "^29.1.1", - "lint-staged": "^16.4.0", + "jsdom": "^29.0.1", + "lint-staged": "^16.2.7", "node-loader": "^2.1.0", - "prettier": "^3.8.3", + "prettier": "^3.8.1", "tailwindcss": "^4", - "typescript": "^6.0.3", - "typescript-eslint": "^8.59.1", - "vitest": "^4.1.5", - "wait-on": "^9.0.5", + "typescript": "^6.0.2", + "typescript-eslint": "^8.56.0", + "vitest": "^4.1.2", + "wait-on": "^9.0.4", "wtfnode": "^0.10.1" }, "engines": { - "node": ">=20.20.2 <21 || >=22.22.2 <23 || >=24.0.0 <27" + "node": ">=20.20.2 <21 || >=22.22.2 <23 || >=24.0.0 <25" }, "optionalDependencies": { - "keytar": "^7.9.0", - "wreq-js": "^2.3.0" + "keytar": "^7.9.0" } }, "node_modules/@adobe/css-tools": { @@ -1487,9 +1487,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "version": "1.19.13", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", + "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -5453,12 +5453,12 @@ } }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.16.0", + "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -8519,9 +8519,9 @@ "license": "MIT" }, "node_modules/hono": { - "version": "4.12.16", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz", - "integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==", + "version": "4.12.14", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", + "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -9570,9 +9570,9 @@ } }, "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "version": "29.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.0.tgz", + "integrity": "sha512-YNUc7fB9QuvSSQWfrH0xF+TyABkxUwx8sswgIDaCrw4Hol8BghdZDkITtZheRJeMtzWlnTfsM3bBBusRvpO1wg==", "dev": true, "license": "MIT", "dependencies": { @@ -11973,9 +11973,9 @@ } }, "node_modules/postcss": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", - "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "funding": [ { "type": "opencollective", @@ -14220,9 +14220,9 @@ } }, "node_modules/undici": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.2.0.tgz", - "integrity": "sha512-Z+4Hx9GE26Lh9Upwfnc8C7SsrpBPGaM/Gm6kMFtiG7c+5IvQKlXi/t+9x9DrrCh29cww5TSP9YdVaBcnLDs5fQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.1.0.tgz", + "integrity": "sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -14991,7 +14991,6 @@ "arm64" ], "license": "MIT", - "optional": true, "os": [ "darwin", "linux", @@ -15192,9 +15191,9 @@ } }, "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index b70e6f1d9b..12d4fc61f0 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "open-sse" ], "engines": { - "node": ">=20.20.2 <21 || >=22.22.2 <23 || >=24.0.0 <27" + "node": ">=20.20.2 <21 || >=22.22.2 <23 || >=24.0.0 <25" }, "keywords": [ "ai", @@ -109,29 +109,29 @@ "system-info": "node scripts/system-info.mjs" }, "dependencies": { - "@lobehub/icons": "^5.6.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@lobehub/icons": "^5.5.4", + "@modelcontextprotocol/sdk": "^1.27.1", "@monaco-editor/react": "^4.7.0", "@ngrok/ngrok": "^1.7.0", "@swc/helpers": "0.5.21", - "axios": "^1.16.0", + "axios": "^1.15.0", "bcryptjs": "^3.0.3", - "better-sqlite3": "^12.9.0", + "better-sqlite3": "^12.6.2", "bottleneck": "^2.19.5", "express": "^5.2.1", - "fetch-socks": "^1.3.3", + "fetch-socks": "^1.3.2", "http-proxy-middleware": "^3.0.5", "https-proxy-agent": "^9.0.0", - "jose": "^6.2.3", - "js-yaml": "^4.1.1", + "jose": "^6.1.3", + "js-yaml": "^4.1.0", "jsonc-parser": "^3.3.1", "lowdb": "^7.0.1", "monaco-editor": "^0.55.1", - "next": "^16.2.4", + "next": "^16.2.3", "next-intl": "^4.11.0", "node-machine-id": "^1.1.12", "open": "^11.0.0", - "ora": "^9.4.0", + "ora": "^9.1.0", "pino": "^10.3.1", "pino-abstract-transport": "^3.0.0", "pino-pretty": "^13.1.3", @@ -139,48 +139,48 @@ "react-dom": "19.2.5", "react-is": "^19.2.5", "react-markdown": "^10.1.0", - "recharts": "^3.8.1", + "recharts": "^3.7.0", "selfsigned": "^5.5.0", "tls-client-node": "^0.1.13", "tsx": "^4.21.0", - "undici": "^8.2.0", + "undici": "^8.1.0", "uuid": "^14.0.0", + "wreq-js": "^2.0.1", "xxhash-wasm": "^1.1.0", "yazl": "^3.3.1", - "zod": "^4.4.3", - "zustand": "^5.0.12" + "zod": "^4.3.6", + "zustand": "^5.0.10" }, "optionalDependencies": { - "keytar": "^7.9.0", - "wreq-js": "^2.3.0" + "keytar": "^7.9.0" }, "devDependencies": { - "@playwright/test": "^1.59.1", - "@tailwindcss/postcss": "^4.2.4", + "@playwright/test": "^1.58.2", + "@tailwindcss/postcss": "^4.1.18", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/bcryptjs": "^3.0.0", "@types/better-sqlite3": "^7.6.13", - "@types/keytar": "^4.4.2", - "@types/node": "^25.6.0", + "@types/keytar": "^4.4.0", + "@types/node": "^25.2.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "c8": "^11.0.0", "concurrently": "^9.2.1", "cross-env": "^10.1.0", - "eslint": "^9.39.4", + "eslint": "^9.39.2", "eslint-config-next": "16.2.4", "husky": "^9.1.7", - "jsdom": "^29.1.1", - "lint-staged": "^16.4.0", + "jsdom": "^29.0.1", + "lint-staged": "^16.2.7", "node-loader": "^2.1.0", - "prettier": "^3.8.3", + "prettier": "^3.8.1", "tailwindcss": "^4", - "typescript": "^6.0.3", - "typescript-eslint": "^8.59.1", - "vitest": "^4.1.5", - "wait-on": "^9.0.5", + "typescript": "^6.0.2", + "typescript-eslint": "^8.56.0", + "vitest": "^4.1.2", + "wait-on": "^9.0.4", "wtfnode": "^0.10.1" }, "lint-staged": { @@ -205,10 +205,10 @@ "overrides": { "lodash-es": "^4.18.1", "dompurify": "^3.4.2", - "path-to-regexp": "^8.4.2", - "postcss": "^8.5.13", - "hono": "^4.12.16", - "@hono/node-server": "^1.19.14", + "path-to-regexp": "^8.4.0", + "postcss": "^8.5.10", + "hono": "^4.12.14", + "@hono/node-server": "^1.19.13", "react": "$react", "react-dom": "$react-dom" } diff --git a/src/app/api/usage/analytics/route.ts b/src/app/api/usage/analytics/route.ts index d284e1c876..e89d9351e2 100644 --- a/src/app/api/usage/analytics/route.ts +++ b/src/app/api/usage/analytics/route.ts @@ -64,44 +64,6 @@ function findKeyInsensitive(obj: Record | undefined | null, key: st return obj[key.toLowerCase()]; } -function uniqueValues(values: Array): string[] { - const seen = new Set(); - const result: string[] = []; - for (const value of values) { - const normalized = typeof value === "string" ? value.trim().toLowerCase() : ""; - if (!normalized || seen.has(normalized)) continue; - seen.add(normalized); - result.push(normalized); - } - return result; -} - -function stripCodexEffortSuffix(model: string): string { - return model.replace(/-(?:xhigh|high|medium|low|none)$/i, ""); -} - -function getPricingModelCandidates( - model: string, - normalizeModelName: (model: string) => string -): string[] { - const normalizedModel = normalizeModelName(model); - const lowerModel = model.toLowerCase(); - const lowerNormalized = normalizedModel.toLowerCase(); - const hyphenModel = lowerModel.replace(/\./g, "-"); - const hyphenNormalized = lowerNormalized.replace(/\./g, "-"); - const effortBaseModel = stripCodexEffortSuffix(lowerNormalized); - - return uniqueValues([ - lowerModel, - lowerNormalized, - hyphenModel, - hyphenNormalized, - effortBaseModel, - effortBaseModel.replace(/\./g, "-"), - lowerNormalized === "codex-auto-review" ? "gpt-5.5" : null, - ]); -} - function resolveModelPricing( pricingByProvider: PricingByProvider, providerAliasMap: Record, @@ -129,15 +91,22 @@ function resolveModelPricing( if (pLower === "antigravity") providerPricing = findKeyInsensitive(pricingByProvider, "ag"); } - const modelCandidates = getPricingModelCandidates(model, normalizeModelName); + const normalizedModel = normalizeModelName(model).toLowerCase(); + const shortModel = normalizedModel; // normalizeModelName behaves exactly like shortModelName + const hyphenModel = model.toLowerCase().replace(/\./g, "-"); + const hyphenNormalized = normalizedModel.replace(/\./g, "-"); + const lowerModel = model.toLowerCase(); const tryFind = (prov: Record | null | undefined) => { if (!prov || typeof prov !== "object") return null; - for (const candidate of modelCandidates) { - const pricing = findKeyInsensitive(prov as Record, candidate); - if (pricing) return pricing; - } - return null; + return ( + findKeyInsensitive(prov as Record, lowerModel) || + findKeyInsensitive(prov as Record, normalizedModel) || + findKeyInsensitive(prov as Record, shortModel) || + findKeyInsensitive(prov as Record, hyphenModel) || + findKeyInsensitive(prov as Record, hyphenNormalized) || + null + ); }; let pricing = providerPricing ? tryFind(providerPricing) : null; @@ -509,20 +478,10 @@ export async function GET(request: Request) { COUNT(*) as total, SUM(CASE WHEN requested_model IS NOT NULL AND requested_model != '' THEN 1 ELSE 0 END) as with_requested, SUM(CASE - WHEN (combo_name IS NULL OR combo_name = '') - AND requested_model IS NOT NULL + WHEN requested_model IS NOT NULL AND requested_model != '' AND model IS NOT NULL - AND model != '' - THEN 1 ELSE 0 END - ) as fallback_eligible, - SUM(CASE - WHEN (combo_name IS NULL OR combo_name = '') - AND requested_model IS NOT NULL - AND requested_model != '' - AND model IS NOT NULL - AND model != '' - AND LOWER(requested_model) != LOWER(model) + AND requested_model != model THEN 1 ELSE 0 END ) as fallbacks FROM call_logs @@ -556,11 +515,10 @@ export async function GET(request: Request) { lastRequest: summaryRow?.lastRequest || "", fallbackCount: Number(fallbackRow?.fallbacks || 0), fallbackRatePct: - Number(fallbackRow?.fallback_eligible || 0) > 0 + Number(fallbackRow?.with_requested || 0) > 0 ? Number( ( - (Number(fallbackRow?.fallbacks || 0) / - Number(fallbackRow?.fallback_eligible || 1)) * + (Number(fallbackRow?.fallbacks || 0) / Number(fallbackRow?.with_requested || 1)) * 100 ).toFixed(2) )