From 6e9c97fbff8503e58a1cb79a99ea833fa2f08262 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 29 Mar 2026 16:39:10 -0300 Subject: [PATCH] =?UTF-8?q?chore(release):=20v3.3.1=20=E2=80=94=20bug=20fi?= =?UTF-8?q?xes=20and=20schema=20adjustments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++ docs/openapi.yaml | 2 +- open-sse/executors/gemini-cli.ts | 1 + .../translator/request/openai-responses.ts | 39 ++++++++++------- package-lock.json | 4 +- package.json | 2 +- .../settings/components/SystemStorageTab.tsx | 15 ++++--- src/app/api/db-backups/import/route.ts | 43 +++++++++++++------ src/shared/services/opencodeConfig.ts | 4 +- ...40-opencode-cli-tools-integration.test.mjs | 8 ++-- 10 files changed, 84 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b89488c7df..dbd6bb89ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ --- +## [3.3.1] - 2026-03-29 + +### 🐛 Bug Fixes + +- **OpenAI Codex:** Fallback processing fix for `type: "text"` elements carrying null or empty datasets that caused 400 rejection (#742). +- **Opencode:** Update schema alignment to singular `provider` to match official spec (#774). +- **Gemini CLI:** Inject missing end-user quota headers preventing 403 authorization lockouts (#775). +- **DB Recovery:** Refactor multipart payload imports into raw binary buffered arrays to bypass reverse proxy max body limits (#770). + +--- + ## [3.3.0] - 2026-03-29 ### ✨ Enhancements & Refactoring diff --git a/docs/openapi.yaml b/docs/openapi.yaml index e63dd1247f..eebf8d849c 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.3.0 + version: 3.3.1 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, diff --git a/open-sse/executors/gemini-cli.ts b/open-sse/executors/gemini-cli.ts index 1d3e064c76..d1a80db8c3 100644 --- a/open-sse/executors/gemini-cli.ts +++ b/open-sse/executors/gemini-cli.ts @@ -21,6 +21,7 @@ export class GeminiCLIExecutor extends BaseExecutor { "User-Agent": `GeminiCLI/0.31.0/${this._currentModel || "unknown"} (linux; x64)`, "X-Goog-Api-Client": "google-genai-sdk/1.41.0 gl-node/v22.19.0", ...(stream && { Accept: "text/event-stream" }), + ...(credentials?.projectId && { "x-goog-user-project": credentials.projectId }), }; } diff --git a/open-sse/translator/request/openai-responses.ts b/open-sse/translator/request/openai-responses.ts index 7b8086b3c6..6bc09b829f 100644 --- a/open-sse/translator/request/openai-responses.ts +++ b/open-sse/translator/request/openai-responses.ts @@ -162,9 +162,10 @@ export function openaiResponsesToOpenAIRequest( type: "function", function: { name: fnName, - arguments: typeof item.arguments === "string" - ? item.arguments - : JSON.stringify(item.arguments ?? {}), + arguments: + typeof item.arguments === "string" + ? item.arguments + : JSON.stringify(item.arguments ?? {}), }, }); currentAssistantMsg.tool_calls = toolCalls; @@ -247,7 +248,11 @@ export function openaiResponsesToOpenAIRequest( }); // Translate tool_choice object format: Responses {type,name} → Chat {type,function:{name}} - if (result.tool_choice && typeof result.tool_choice === "object" && !Array.isArray(result.tool_choice)) { + if ( + result.tool_choice && + typeof result.tool_choice === "object" && + !Array.isArray(result.tool_choice) + ) { const tc = toRecord(result.tool_choice); const tcType = toString(tc.type); if (tcType === "function" && tc.name !== undefined && !tc.function) { @@ -322,7 +327,9 @@ export function openaiToOpenAIResponsesRequest( return { type: "input_text", text: toString(contentItem.text) }; } if (contentItem.type === "image_url") { - const imgUrl = contentItem.image_url as string | { url?: string; detail?: string }; + const imgUrl = contentItem.image_url as + | string + | { url?: string; detail?: string }; const imgResult: JsonRecord = { type: "input_image", image_url: typeof imgUrl === "string" ? imgUrl : imgUrl?.url || "", @@ -367,7 +374,7 @@ export function openaiToOpenAIResponsesRequest( } else if (Array.isArray(msg.content)) { for (const contentValue of msg.content) { const contentItem = toRecord(contentValue); - if (contentItem.type === "text" && contentItem.text) { + if (contentItem.type === "text") { outputContent.push({ type: "output_text", text: toString(contentItem.text) }); } else if (contentItem.type === "thinking" || contentItem.type === "redacted_thinking") { // Reasoning already moved above @@ -426,15 +433,17 @@ export function openaiToOpenAIResponsesRequest( input.push({ type: "function_call_output", call_id: toString(msg.tool_call_id), - output: typeof msg.content === "string" - ? msg.content - : Array.isArray(msg.content) - ? msg.content.map((c) => { - const part = toRecord(c); - if (part.type === "text") return { type: "input_text", text: toString(part.text) }; - return c; - }) - : String(msg.content ?? ""), + output: + typeof msg.content === "string" + ? msg.content + : Array.isArray(msg.content) + ? msg.content.map((c) => { + const part = toRecord(c); + if (part.type === "text") + return { type: "input_text", text: toString(part.text) }; + return c; + }) + : String(msg.content ?? ""), }); } diff --git a/package-lock.json b/package-lock.json index de44509bc9..6004a1ad94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.3.0", + "version": "3.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.3.0", + "version": "3.3.1", "hasInstallScript": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index 06d800ab92..710a594aa6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.3.0", + "version": "3.3.1", "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": { diff --git a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx index 66cda1bcaf..342cb619e1 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx @@ -244,12 +244,15 @@ export default function SystemStorageTab() { setImportStatus({ type: "", message: "" }); setConfirmImport(false); try { - const formData = new FormData(); - formData.append("file", pendingImportFile); - const res = await fetch("/api/db-backups/import", { - method: "POST", - body: formData, - }); + const arrayBuffer = await pendingImportFile.arrayBuffer(); + const res = await fetch( + `/api/db-backups/import?filename=${encodeURIComponent(pendingImportFile.name)}`, + { + method: "POST", + headers: { "Content-Type": "application/octet-stream" }, + body: arrayBuffer, + } + ); const data = await res.json(); if (res.ok) { setImportStatus({ diff --git a/src/app/api/db-backups/import/route.ts b/src/app/api/db-backups/import/route.ts index a988b30f38..40f052bea0 100644 --- a/src/app/api/db-backups/import/route.ts +++ b/src/app/api/db-backups/import/route.ts @@ -29,18 +29,34 @@ export async function POST(request: Request) { let tmpPath: string | null = null; try { - const formData = await request.formData(); - const file = formData.get("file") as File | null; + let fileBuffer: Buffer | null = null; + let fileName = ""; + const contentType = request.headers.get("content-type") || ""; - if (!file) { - return NextResponse.json( - { error: "No file provided. Upload a .sqlite file." }, - { status: 400 } - ); + if (contentType.includes("multipart/form-data")) { + const formData = await request.formData(); + const file = formData.get("file") as File | null; + if (!file) { + return NextResponse.json( + { error: "No file provided. Upload a .sqlite file." }, + { status: 400 } + ); + } + fileName = file.name; + fileBuffer = Buffer.from(await file.arrayBuffer()); + } else { + // Direct binary transfer to bypass Reverse Proxy / Next.js FormData parsing errors under chunked encoding (Bug #770) + const buffer = await request.arrayBuffer(); + if (!buffer || buffer.byteLength === 0) { + return NextResponse.json({ error: "No file content provided." }, { status: 400 }); + } + fileBuffer = Buffer.from(buffer); + const url = new URL(request.url); + fileName = url.searchParams.get("filename") || "import.sqlite"; } // Validate filename extension - if (!file.name.endsWith(".sqlite")) { + if (!fileName.endsWith(".sqlite")) { return NextResponse.json( { error: "Invalid file type. Only .sqlite files are accepted." }, { status: 400 } @@ -48,14 +64,15 @@ export async function POST(request: Request) { } // Validate file size - if (file.size > MAX_UPLOAD_SIZE) { + const fileSize = fileBuffer.length; + if (fileSize > MAX_UPLOAD_SIZE) { return NextResponse.json( { error: `File too large. Maximum allowed size is ${MAX_UPLOAD_SIZE / (1024 * 1024)} MB.` }, { status: 400 } ); } - if (file.size < 4096) { + if (fileSize < 4096) { return NextResponse.json( { error: "File too small to be a valid SQLite database." }, { status: 400 } @@ -63,10 +80,8 @@ export async function POST(request: Request) { } // Write uploaded file to temp location - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); tmpPath = path.join(os.tmpdir(), `omniroute-import-${Date.now()}.sqlite`); - fs.writeFileSync(tmpPath, buffer); + fs.writeFileSync(tmpPath, fileBuffer!); // Validate SQLite integrity let testDb: InstanceType | null = null; @@ -141,7 +156,7 @@ export async function POST(request: Request) { return NextResponse.json({ imported: true, - filename: file.name, + filename: fileName, connectionCount: connCount, nodeCount, comboCount, diff --git a/src/shared/services/opencodeConfig.ts b/src/shared/services/opencodeConfig.ts index da4305ea23..65bd044f14 100644 --- a/src/shared/services/opencodeConfig.ts +++ b/src/shared/services/opencodeConfig.ts @@ -56,8 +56,8 @@ export const mergeOpenCodeConfig = ( return { ...safeConfig, - providers: { - ...((safeConfig as any).providers || {}), + provider: { + ...((safeConfig as any).provider || {}), omniroute: buildOpenCodeProviderConfig(input), }, }; diff --git a/tests/unit/t40-opencode-cli-tools-integration.test.mjs b/tests/unit/t40-opencode-cli-tools-integration.test.mjs index 94dfb38755..f8d79dc477 100644 --- a/tests/unit/t40-opencode-cli-tools-integration.test.mjs +++ b/tests/unit/t40-opencode-cli-tools-integration.test.mjs @@ -54,16 +54,16 @@ test("T40: OpenCode config generator includes endpoint and selected API key", () assert.ok(providerConfig.models.includes("claude-sonnet-4-5-thinking")); const mergedConfig = mergeOpenCodeConfig( - { providers: { custom: { name: "Custom Provider" } } }, + { provider: { custom: { name: "Custom Provider" } } }, { baseUrl: "http://localhost:20128/v1", apiKey: "sk_test_opencode", model: "claude-sonnet-4-5-thinking", } ); - assert.ok(mergedConfig.providers.custom); - assert.equal(mergedConfig.providers.omniroute.baseURL, "http://localhost:20128/v1"); - assert.equal(mergedConfig.providers.omniroute.apiKey, "sk_test_opencode"); + assert.ok(mergedConfig.provider.custom); + assert.equal(mergedConfig.provider.omniroute.baseURL, "http://localhost:20128/v1"); + assert.equal(mergedConfig.provider.omniroute.apiKey, "sk_test_opencode"); }); test("T40: Windsurf card documents current official limitations honestly", () => {