Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 227268024d | |||
| 8d5891a382 | |||
| 7700fca501 | |||
| 527c542d6d |
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user