chore(release): v3.3.1 — bug fixes and schema adjustments

This commit is contained in:
diegosouzapw
2026-03-29 16:39:10 -03:00
parent af338d447b
commit 6e9c97fbff
10 changed files with 84 additions and 45 deletions

View File

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

View File

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

View File

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

View File

@@ -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 ?? ""),
});
}

4
package-lock.json generated
View File

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

View File

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

View File

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

View File

@@ -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<typeof Database> | 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,

View File

@@ -56,8 +56,8 @@ export const mergeOpenCodeConfig = (
return {
...safeConfig,
providers: {
...((safeConfig as any).providers || {}),
provider: {
...((safeConfig as any).provider || {}),
omniroute: buildOpenCodeProviderConfig(input),
},
};

View File

@@ -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", () => {