mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
chore(release): v3.7.9 — all changes in ONE commit
This commit is contained in:
@@ -18,6 +18,10 @@
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(ui):** restore Next.js CSP `unsafe-eval` in production `script-src` to fix unresponsive Onboarding button (fixes #1883)
|
||||
- **fix(proxy):** globally strip `prompt_cache_retention` in `BaseExecutor` to prevent upstream 400 errors from strict endpoints like droid/gemini-2-pro (fixes #1884)
|
||||
- **fix(ui):** include `isOpen` dependency in `EditConnectionModal` state sync to ensure `maxConcurrent` is properly hydrated when reopening the modal (fixes #1859)
|
||||
- **fix(security):** remediate 4 polynomial-redos CodeQL alerts in compression regexes by bounding repetitions and removing overlapping quantifiers
|
||||
- **fix(codex):** flatten Chat Completions tool format to Codex Responses format in `normalizeCodexTools` — prevents `Missing required parameter: tools[0].name` upstream errors (#1914 — thanks @tranduykhanh030)
|
||||
- **fix(proxy):** add proxy-aware execution context to image generation route — proxy settings are now correctly applied for image providers behind restricted networks (#1904 — thanks @Aculeasis)
|
||||
- **fix(translator):** inject `properties: {}` into zero-argument MCP tool schemas during Anthropic→OpenAI translation — prevents 400 errors from OpenAI strict schema validation (#1898 — thanks @bryceIT)
|
||||
|
||||
@@ -8,7 +8,7 @@ const projectRoot = dirname(fileURLToPath(import.meta.url));
|
||||
const scriptSrc =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:"
|
||||
: "script-src 'self' 'unsafe-inline' blob:";
|
||||
: "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:";
|
||||
const contentSecurityPolicy = [
|
||||
"default-src 'self'",
|
||||
"base-uri 'self'",
|
||||
|
||||
@@ -293,6 +293,9 @@ export class BaseExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
// Fix #1884: Cursor sends prompt_cache_retention which breaks strict upstream endpoints
|
||||
delete cloned.prompt_cache_retention;
|
||||
|
||||
// Also clean up top level optional fields that commonly cause issues when empty
|
||||
const optionalKeys = ["user", "stop", "seed", "response_format"];
|
||||
for (const key of optionalKeys) {
|
||||
|
||||
@@ -1425,7 +1425,7 @@ export class CodexExecutor extends BaseExecutor {
|
||||
delete body.seed;
|
||||
// max_tokens and max_output_tokens already deleted above (before passthrough return)
|
||||
delete body.user; // Cursor sends this but Codex doesn't support it
|
||||
delete body.prompt_cache_retention; // Cursor sends this but Codex doesn't support it
|
||||
|
||||
delete body.metadata; // Cursor sends this but Codex doesn't support it
|
||||
delete body.stream_options; // Cursor sends this but Codex doesn't support it
|
||||
delete body.safety_identifier; // Droid CLI sends this but Codex doesn't support it
|
||||
|
||||
@@ -65,9 +65,9 @@ export function compressMcpDescription(description: string): DescriptionCompress
|
||||
const applied = applyRulesToText(text, rules).text;
|
||||
const normalized = applied
|
||||
.replace(/[ \t]{2,}/g, " ")
|
||||
.replace(/\s+([,.;:!?])/g, "$1")
|
||||
.replace(/[ \t]+([,.;:!?])/g, "$1")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.replace(/(^|[.!?]\s+)([a-z])/g, (_match, prefix: string, char: string) => {
|
||||
.replace(/(^|[.!?][ \t]+|\n+[ \t]*)([a-z])/g, (_match, prefix: string, char: string) => {
|
||||
return `${prefix}${char.toUpperCase()}`;
|
||||
})
|
||||
.trim();
|
||||
|
||||
@@ -204,8 +204,8 @@ export function applyRulesToText(
|
||||
|
||||
function cleanupArtifacts(text: string): string {
|
||||
let result = text;
|
||||
if (result.includes(" ")) result = result.replace(/ +/g, " ");
|
||||
if (/[\t ]+[,.;:!?]/.test(result)) result = result.replace(/\s+([,.;:!?])/g, "$1");
|
||||
if (result.includes(" ")) result = result.replace(/[ \t]{2,}/g, " ");
|
||||
if (/[\t ]+[,.;:!?]/.test(result)) result = result.replace(/[ \t]+([,.;:!?])/g, "$1");
|
||||
if (/[.!?]{2,}/.test(result)) result = result.replace(/([.!?]){2,}/g, "$1");
|
||||
if (/[ \t]\n/.test(result)) result = result.replace(/[ \t]+$/gm, "");
|
||||
if (result.endsWith(" ") || result.endsWith("\t")) result = result.trimEnd();
|
||||
@@ -216,9 +216,12 @@ function cleanupArtifacts(text: string): string {
|
||||
}
|
||||
|
||||
function recapitalizeSentences(text: string): string {
|
||||
return text.replace(/(^|[.!?]\s+|\n+\s*)([a-z])/g, (_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(
|
||||
@@ -262,7 +265,7 @@ function compileUserPreservePatterns(patterns: string[]): {
|
||||
}
|
||||
|
||||
const PROTECTED_STRUCTURE_RE =
|
||||
/```|~~~|`|https?:\/\/|\[[^\]\n]+\]\([^) \n]+(?:\s+"[^"]*")?\)|^#{1,6}\s+|^\s*\|.*\|\s*$|\$\$|\\\[|\\begin\{|^\s*#(?:set|show|let|import|include)\b|\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b|\bprocess\.env\.[A-Za-z_][A-Za-z0-9_]*\b|\$[A-Z_][A-Z0-9_]*\b|\b\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9.-]+)?\b|\b[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)+\(\)?|\b[A-Za-z_$][\w$]*\s*\([^()\n]*\)|(?:^|\s)(?:\.{0,2}\/[A-Za-z0-9_@./-]+|[A-Za-z]:\\[A-Za-z0-9_.\\/-]+)|\b(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError|EvalError|Error|Exception):[^\n]+/im;
|
||||
/```|~~~|`|https?:\/\/|\[[^\]\n]{1,1000}\]\([^)[ \t\n]{1,2000}(?:[ \t]+"[^"]{0,1000}")?\)|^#{1,6}\s+|^[ \t]*\|(?:[^|\n]{0,1000}\|){1,100}[ \t]*$|\$\$|\\\[|\\begin\{|^\s*#(?:set|show|let|import|include)\b|\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b|\bprocess\.env\.[A-Za-z_][A-Za-z0-9_]*\b|\$[A-Z_][A-Z0-9_]*\b|\b\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9.-]+)?\b|\b[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)+\(\)?|\b[A-Za-z_$][\w$]*[ \t]*\([^()\n]{0,1000}\)|(?:^|\s)(?:\.{0,2}\/[A-Za-z0-9_@./-]+|[A-Za-z]:\\[A-Za-z0-9_.\\/-]+)|\b(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError|EvalError|Error|Exception):[^\n]{0,1000}/im;
|
||||
const PROTECTED_STRUCTURE_PREFILTER_RE = /[`~\[\]\|$#\\/:_()0-9]/;
|
||||
|
||||
function hasProtectedStructure(text: string): boolean {
|
||||
|
||||
@@ -59,7 +59,7 @@ export function validateCompression(original: string, compressed: string): Valid
|
||||
);
|
||||
requireExactPresence(
|
||||
"markdown link",
|
||||
collectMatches(original, /\[[^\]\n]+\]\([^) \n]+(?:\s+"[^"]*")?\)/g),
|
||||
collectMatches(original, /\[[^\]\n]{1,1000}\]\([^)[ \t\n]{1,2000}(?:[ \t]+"[^"]{0,1000}")?\)/g),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
@@ -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, /^\s*\|.*\|\s*$/gm),
|
||||
collectMatches(original, /^[ \t]*\|(?:[^|\n]{0,1000}\|){1,100}[ \t]*$/gm),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
requireExactPresence(
|
||||
"math block",
|
||||
collectMatches(original, /\$\$[\s\S]*?\$\$/g),
|
||||
collectMatches(original, /\$\$[\s\S]{0,10000}?\$\$/g),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
@@ -85,7 +85,7 @@ export function validateCompression(original: string, compressed: string): Valid
|
||||
);
|
||||
requireExactPresence(
|
||||
"LaTeX block",
|
||||
collectMatches(original, /\\begin\{[A-Za-z*]+\}[\s\S]*?\\end\{[A-Za-z*]+\}/g),
|
||||
collectMatches(original, /\\begin\{[A-Za-z*]{1,50}\}[\s\S]{0,10000}?\\end\{[A-Za-z*]{1,50}\}/g),
|
||||
compressed,
|
||||
errors
|
||||
);
|
||||
|
||||
@@ -6054,7 +6054,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
: t("leaveBlankKeepCurrentApiKey");
|
||||
|
||||
useEffect(() => {
|
||||
if (connection) {
|
||||
if (isOpen && connection) {
|
||||
const rawBaseUrl = connection.providerSpecificData?.baseUrl;
|
||||
const existingBaseUrl = typeof rawBaseUrl === "string" ? rawBaseUrl : "";
|
||||
const rawRegion = connection.providerSpecificData?.region;
|
||||
@@ -6076,9 +6076,9 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
name: connection.name || "",
|
||||
priority: connection.priority || 1,
|
||||
maxConcurrent:
|
||||
connection.maxConcurrent === null || connection.maxConcurrent === undefined
|
||||
? ""
|
||||
: String(connection.maxConcurrent),
|
||||
connection.maxConcurrent !== null && connection.maxConcurrent !== undefined
|
||||
? String(connection.maxConcurrent)
|
||||
: "",
|
||||
apiKey: "",
|
||||
healthCheckInterval: connection.healthCheckInterval ?? 60,
|
||||
baseUrl: existingBaseUrl || defaultBaseUrl,
|
||||
@@ -6115,7 +6115,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
setValidationResult(null);
|
||||
setSaveError(null);
|
||||
}
|
||||
}, [connection, defaultBaseUrl, isVertex]);
|
||||
}, [isOpen, connection, defaultBaseUrl, isVertex]);
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!connection?.provider) return;
|
||||
|
||||
@@ -14,7 +14,7 @@ test("system sidebar items place logs before health", () => {
|
||||
assert.ok(systemSection, "expected system sidebar section to exist");
|
||||
assert.deepEqual(
|
||||
systemSection.items.map((item) => item.id),
|
||||
["logs", "audit", "webhooks", "health", "settings"]
|
||||
["logs", "audit", "webhooks", "health", "proxy", "settings"]
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -124,7 +124,9 @@ test("GET /api/usage/analytics includes byModel array with cost calculations", a
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(Array.isArray(body.byModel));
|
||||
assert.ok(body.byModel.length > 0);
|
||||
const gptEntry = body.byModel.find((m) => m.model === "4o" && m.provider === "openai");
|
||||
const gptEntry = body.byModel.find(
|
||||
(m) => (m.model === "4o" || m.model === "gpt-4o") && m.provider === "openai"
|
||||
);
|
||||
assert.ok(gptEntry);
|
||||
assert.ok(typeof gptEntry.cost === "number");
|
||||
assert.ok(gptEntry.cost > 0);
|
||||
@@ -200,7 +202,7 @@ test("GET /api/usage/analytics does not persist guessed API key attribution", as
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(body.byApiKey.some((row) => row.apiKeyName === "Unknown API key"));
|
||||
assert.equal(body.byApiKey.length, 0);
|
||||
|
||||
const row = db
|
||||
.prepare("SELECT api_key_id, api_key_name FROM usage_history WHERE connection_id = ?")
|
||||
|
||||
Reference in New Issue
Block a user