Compare commits

..

4 Commits

Author SHA1 Message Date
diegosouzapw 227268024d chore: bump version to 1.7.7 and update CHANGELOG
Build Electron Desktop App / Validate version (push) Failing after 28s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
2026-03-02 10:29:08 -03:00
diegosouzapw 8d5891a382 fix: sanitize tool schemas for Gemini provider (#173)
- Added cleanJSONSchemaForAntigravity() to openaiToGeminiBase() tool conversion
- Both OpenAI-format and Claude-format tool parameters are now sanitized
- Also sanitized response_format json_schema using the same function
- Removes unsupported JSON Schema keywords (additionalProperties, $schema, etc.)
- All Gemini paths (standard, CLI, Antigravity) now consistently sanitize schemas
2026-03-02 10:28:26 -03:00
diegosouzapw 7700fca501 chore: bump version to 1.7.6 and update CHANGELOG
Build Electron Desktop App / Validate version (push) Failing after 28s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
2026-03-02 10:19:43 -03:00
diegosouzapw 527c542d6d fix: cloud proxy endpoint shows undefined/v1 when env var not set (#171)
- syncAndVerify now returns cloudUrl in API response for frontend to use
- EndpointPageClient uses dynamic cloudBaseUrl state instead of relying on env var
- Falls back gracefully when NEXT_PUBLIC_CLOUD_URL is not set (Docker deployments)
- Fixed setInterval in accountFallback.ts global scope for Cloudflare Workers compat
2026-03-02 10:18:21 -03:00
7 changed files with 63 additions and 18 deletions
+20
View File
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
---
## [1.7.7] — 2026-03-02
### 🐛 Bug Fixes
- **Gemini Tool Schema Sanitization** — The standard Gemini provider now sanitizes OpenAI tool schemas before forwarding to Gemini API, removing unsupported JSON Schema keywords (`additionalProperties`, `$schema`, `const`, `default`, `not`, etc.). Previously, sanitization only ran in the CLI executor path, causing Gemini to reject tool calls when schemas contained unsupported constraints. Also applied sanitization to `response_format.json_schema`. Fixes #173
## [1.7.6] — 2026-03-02
### 🐛 Bug Fixes
- **Cloud Proxy `undefined/v1` Fix** — When the `NEXT_PUBLIC_CLOUD_URL` environment variable is not set (common in Docker deployments), the endpoint page now correctly falls back instead of showing `undefined/v1`. The cloud sync API now returns `cloudUrl` in its response so the frontend can use it dynamically. Fixes #171
### ✨ New Features
- **Cloud Worker `/v1/models` Endpoint** — The Cloud Worker now supports the `/v1/models` endpoint for both URL formats (`/v1/models` and `/{machineId}/v1/models`), returning all available models synced from the local OmniRoute instance
### 🔧 Infrastructure
- **Cloudflare Workers Compatibility** — Fixed `setInterval` in global scope issue in `accountFallback.ts` that blocked Cloud Worker deployment. Lazy initialization pattern ensures compatibility with Cloudflare Workers runtime restrictions
## [1.7.5] — 2026-03-02
### 🐛 Bug Fixes
+19 -7
View File
@@ -24,14 +24,25 @@ export function getProviderProfile(provider) {
// In-memory map: "provider:connectionId:model" → { reason, until, lockedAt }
const modelLockouts = new Map();
// Auto-cleanup expired lockouts every 15 seconds
const _cleanupTimer = setInterval(() => {
const now = Date.now();
for (const [key, entry] of modelLockouts) {
if (now > entry.until) modelLockouts.delete(key);
// Auto-cleanup expired lockouts every 15 seconds (lazy init for Cloudflare Workers compatibility)
let _cleanupTimer: ReturnType<typeof setInterval> | null = null;
function ensureCleanupTimer() {
if (_cleanupTimer) return;
try {
_cleanupTimer = setInterval(() => {
const now = Date.now();
for (const [key, entry] of modelLockouts) {
if (now > entry.until) modelLockouts.delete(key);
}
}, 15_000);
if (typeof _cleanupTimer === "object" && "unref" in _cleanupTimer) {
(_cleanupTimer as any).unref(); // Don't prevent process exit (Node.js only)
}
} catch {
// Cloudflare Workers may not support setInterval outside handlers — skip cleanup timer
}
}, 15_000);
_cleanupTimer.unref(); // Don't prevent process exit
}
/**
* Lock a specific model on a specific account
@@ -43,6 +54,7 @@ _cleanupTimer.unref(); // Don't prevent process exit
*/
export function lockModel(provider, connectionId, model, reason, cooldownMs) {
if (!model) return; // No model → skip model-level locking
ensureCleanupTimer();
const key = `${provider}:${connectionId}:${model}`;
modelLockouts.set(key, {
reason,
@@ -180,7 +180,9 @@ function openaiToGeminiBase(model, body, stream) {
functionDeclarations.push({
name: t.name,
description: t.description || "",
parameters: t.input_schema || { type: "object", properties: {} },
parameters: cleanJSONSchemaForAntigravity(
t.input_schema || { type: "object", properties: {} }
),
});
}
// OpenAI format
@@ -189,7 +191,9 @@ function openaiToGeminiBase(model, body, stream) {
functionDeclarations.push({
name: fn.name,
description: fn.description || "",
parameters: fn.parameters || { type: "object", properties: {} },
parameters: cleanJSONSchemaForAntigravity(
fn.parameters || { type: "object", properties: {} }
),
});
}
}
@@ -206,9 +210,7 @@ function openaiToGeminiBase(model, body, stream) {
// Extract the schema (may be nested under .schema key)
const schema = body.response_format.json_schema.schema || body.response_format.json_schema;
if (schema && typeof schema === "object") {
// Remove unsupported keywords for Gemini (it uses a subset of JSON Schema)
const { $schema, additionalProperties, ...cleanSchema } = schema;
result.generationConfig.responseSchema = cleanSchema;
result.generationConfig.responseSchema = cleanJSONSchemaForAntigravity(schema);
}
} else if (body.response_format.type === "json_object") {
result.generationConfig.responseMimeType = "application/json";
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "1.7.4",
"version": "1.7.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "1.7.4",
"version": "1.7.7",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "1.7.5",
"version": "1.7.7",
"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": {
@@ -7,7 +7,7 @@ import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers";
import { useTranslations } from "next-intl";
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL || null;
const CLOUD_ACTION_TIMEOUT_MS = 15000;
export default function APIPageClient({ machineId }) {
@@ -29,6 +29,7 @@ export default function APIPageClient({ machineId }) {
const [syncStep, setSyncStep] = useState(""); // "syncing" | "verifying" | "disabling" | "done" | ""
const [modalSuccess, setModalSuccess] = useState(false); // show success state in modal before closing
const [selectedProvider, setSelectedProvider] = useState(null); // for provider models popup
const [cloudBaseUrl, setCloudBaseUrl] = useState(CLOUD_URL); // dynamic cloud URL from API response
const { copied, copy } = useCopyToClipboard();
@@ -204,6 +205,10 @@ export default function APIPageClient({ machineId }) {
if (data.createdKey) {
await fetchData();
}
// Update cloud URL from API response (fixes undefined/v1 when env var not set)
if (data.cloudUrl) {
setCloudBaseUrl(data.cloudUrl);
}
// Reload settings to ensure fresh state
await loadCloudSettings();
} else {
@@ -274,7 +279,7 @@ export default function APIPageClient({ machineId }) {
};
const [baseUrl, setBaseUrl] = useState("/v1");
const cloudEndpointNew = `${CLOUD_URL}/v1`;
const cloudEndpointNew = cloudBaseUrl ? `${cloudBaseUrl}/v1` : null;
// Hydration fix: Only access window on client side
useEffect(() => {
@@ -293,7 +298,7 @@ export default function APIPageClient({ machineId }) {
}
// Use new format endpoint (machineId embedded in key)
const currentEndpoint = cloudEnabled ? cloudEndpointNew : baseUrl;
const currentEndpoint = cloudEnabled && cloudEndpointNew ? cloudEndpointNew : baseUrl;
return (
<div className="flex flex-col gap-8">
+6
View File
@@ -114,11 +114,15 @@ async function syncAndVerify(machineId: string, createdKey: any, existingKeys: a
);
}
// Build the cloud URL for the frontend to use
const cloudUrl = CLOUD_URL ? `${CLOUD_URL}/${machineId}` : null;
// Step 2: Verify connection by pinging the cloud (with retry)
const apiKey = createdKey || existingKeys[0]?.key;
if (!apiKey) {
return NextResponse.json({
...syncResult,
cloudUrl,
verified: false,
verifyError: "No API key available",
});
@@ -146,6 +150,7 @@ async function syncAndVerify(machineId: string, createdKey: any, existingKeys: a
if (pingResponse.ok) {
return NextResponse.json({
...syncResult,
cloudUrl,
verified: true,
});
}
@@ -163,6 +168,7 @@ async function syncAndVerify(machineId: string, createdKey: any, existingKeys: a
// Sync succeeded but verify failed — still return success with warning
return NextResponse.json({
...syncResult,
cloudUrl,
verified: false,
verifyError: lastVerifyError || "Verification failed after retries",
});