Merge main into release/v3.7.9

This commit is contained in:
diegosouzapw
2026-05-04 16:55:04 -03:00
8 changed files with 112 additions and 161 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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();

View File

@@ -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(

View File

@@ -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
);

101
package-lock.json generated
View File

@@ -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"

View File

@@ -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"
}

View File

@@ -64,44 +64,6 @@ function findKeyInsensitive(obj: Record<string, any> | undefined | null, key: st
return obj[key.toLowerCase()];
}
function uniqueValues(values: Array<string | null | undefined>): string[] {
const seen = new Set<string>();
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<string, string>,
@@ -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<string, unknown> | null | undefined) => {
if (!prov || typeof prov !== "object") return null;
for (const candidate of modelCandidates) {
const pricing = findKeyInsensitive(prov as Record<string, unknown>, candidate);
if (pricing) return pricing;
}
return null;
return (
findKeyInsensitive(prov as Record<string, unknown>, lowerModel) ||
findKeyInsensitive(prov as Record<string, unknown>, normalizedModel) ||
findKeyInsensitive(prov as Record<string, unknown>, shortModel) ||
findKeyInsensitive(prov as Record<string, unknown>, hyphenModel) ||
findKeyInsensitive(prov as Record<string, unknown>, 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)
)