mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
chore(release): v3.3.1 — bug fixes and schema adjustments
This commit is contained in:
11
CHANGELOG.md
11
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
4
package-lock.json
generated
@@ -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": [
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -56,8 +56,8 @@ export const mergeOpenCodeConfig = (
|
||||
|
||||
return {
|
||||
...safeConfig,
|
||||
providers: {
|
||||
...((safeConfig as any).providers || {}),
|
||||
provider: {
|
||||
...((safeConfig as any).provider || {}),
|
||||
omniroute: buildOpenCodeProviderConfig(input),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user