Release v3.7.4 (#1730)

* chore(release): v3.7.4 — version bump, openapi and changelog sync

* fix: preserve previous_response_id and conversation_id fields on empty input array (#1729)

* fix: bypass UI validation block for optional API keys and fix string fallback typing (#1721)

* fix(proxy): disable HTTP keep-alive and pipelining in Undici proxy dispatcher to prevent socket hang up

* feat(proxy): implement bulk proxy import via pipe-delimited parser with update-or-create logic

* docs: update changelog for v3.7.4 fixes and proxy features

* test: update responses store expectations for empty input arrays

* feat(pwa): add fullscreen installable PWA with manifest, service worker, and cross-platform app icons. (#1728)

Integrated into release/v3.7.4

* Fix image provider validation and Stability image requests (#1726)

Integrated into release/v3.7.4

* docs: add PR 1726 and PR 1728 to v3.7.4 changelog

* fix(security): replace insecure Math.random with crypto.getRandomValues for fallback UUID generation

* fix(migrations): intercept 007 migration to use IF NOT EXISTS logic on fresh installs

Fixes #1733

* test: fix typescript compilation errors in unit tests

* fix(db): reconcile legacy reasoning cache migration

* chore(release): bump to v3.7.4 — changelog, docs, version sync

* fix(cc-compatible): preserve Claude Code system skeleton (#1740)

Integrated into release/v3.7.4

* docs(changelog): update for PR #1740 merge

* docs(changelog): include workflow updates

* fix(db): reconcile legacy reasoning cache migration (#1734)

Integrated into release/v3.7.4

* Add endpoint tunnel visibility settings (#1743)

Integrated into release/v3.7.4

* Normalize max reasoning effort for Codex routing (#1744)

Integrated into release/v3.7.4

* Fix Claude Code gateway config helper (#1745)

Integrated into release/v3.7.4

* Refresh CLI fingerprint provider profiles (#1746)

Integrated into release/v3.7.4

* Integrated into release/v3.7.4 (PR #1742)

* docs(changelog): update for PRs 1742-1746

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: Yash Ghule <y.ghule77@gmail.com>
Co-authored-by: backryun <bakryun0718@proton.me>
Co-authored-by: dhaern <manker_lol@hotmail.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: Duncan L <leungd@gmail.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-04-28 20:46:25 -03:00
committed by GitHub
parent 4cdd0dfd1a
commit 0cd388efb8
160 changed files with 4957 additions and 684 deletions

View File

@@ -10,6 +10,7 @@ import {
getCliRuntimeStatus,
} from "@/shared/services/cliRuntime";
import { createBackup } from "@/shared/services/backupService";
import { normalizeClaudeBaseUrl } from "@/shared/services/claudeCliConfig";
import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState";
import { cliSettingsEnvSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
@@ -122,7 +123,7 @@ export async function POST(request: Request) {
env.ANTHROPIC_AUTH_TOKEN = keyRecord.key as string;
}
} catch {
// Non-critical: fall back to whatever value was in env (e.g. sk_omniroute)
// Non-critical: fall back to whatever value was already provided in env.
}
}
@@ -146,11 +147,9 @@ export async function POST(request: Request) {
}
}
// Normalize ANTHROPIC_BASE_URL to ensure /v1 suffix
// Claude Code gateway mode expects the unified root endpoint, not a forced /v1 suffix.
if (env.ANTHROPIC_BASE_URL) {
env.ANTHROPIC_BASE_URL = env.ANTHROPIC_BASE_URL.endsWith("/v1")
? env.ANTHROPIC_BASE_URL
: `${env.ANTHROPIC_BASE_URL}/v1`;
env.ANTHROPIC_BASE_URL = normalizeClaudeBaseUrl(env.ANTHROPIC_BASE_URL);
}
// Merge new env with existing settings
@@ -186,6 +185,7 @@ export async function POST(request: Request) {
const RESET_ENV_KEYS = [
"ANTHROPIC_BASE_URL",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_API_KEY",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",

View File

@@ -0,0 +1,75 @@
import { upsertProxy } from "@/lib/localDb";
import { bulkImportProxiesSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return createErrorResponse({
status: 400,
message: "Invalid JSON body",
type: "invalid_request",
});
}
try {
const validation = validateBody(bulkImportProxiesSchema, rawBody);
if (isValidationFailure(validation)) {
return createErrorResponse({
status: 400,
message: validation.error.message,
details: validation.error.details,
type: "invalid_request",
});
}
const { items } = validation.data;
let created = 0;
let updated = 0;
let failed = 0;
const results: Array<{
name: string;
success: boolean;
action?: "created" | "updated";
id?: string;
error?: string;
}> = [];
for (const item of items) {
try {
const result = await upsertProxy(item);
if (result.proxy) {
if (result.action === "created") created++;
else updated++;
results.push({
name: item.name,
success: true,
action: result.action,
id: result.proxy.id,
});
} else {
failed++;
results.push({ name: item.name, success: false, error: "Unknown error" });
}
} catch (error) {
failed++;
results.push({
name: item.name,
success: false,
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
return Response.json({ created, updated, failed, results });
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to bulk import proxies");
}
}

View File

@@ -15,6 +15,7 @@ import {
launchAutoUpdate,
validateAutoUpdateRuntime,
} from "@/lib/system/autoUpdate";
import { NEWS_JSON_URL, parseActiveNewsPayload } from "@/shared/utils/releaseNotes";
const execFileAsync = promisify(execFile);
@@ -50,16 +51,32 @@ function isNewer(a: string | null, b: string): boolean {
return aPat > bPat;
}
async function getNews() {
try {
const res = await fetch(NEWS_JSON_URL, { next: { revalidate: 3600 } });
if (!res.ok) return null;
const data = await res.json();
return parseActiveNewsPayload(data);
} catch {
return null;
}
}
export async function GET(req: NextRequest) {
if (!(await isAuthenticated(req))) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const current = getCurrentVersion();
const latest = await getLatestNpmVersion();
const updateAvailable = isNewer(latest, current);
const config = getAutoUpdateConfig();
const validation = await validateAutoUpdateRuntime(config);
const [latest, news, validation] = await Promise.all([
getLatestNpmVersion(),
getNews(),
validateAutoUpdateRuntime(config),
]);
const updateAvailable = isNewer(latest, current);
return NextResponse.json({
current,
@@ -68,6 +85,7 @@ export async function GET(req: NextRequest) {
channel: config.mode,
autoUpdateSupported: validation.supported,
autoUpdateError: validation.reason,
news,
});
}

View File

@@ -1,10 +1,5 @@
import { handleImageEdit } from "@omniroute/open-sse/handlers/imageGeneration.ts";
import {
getProviderCredentials,
clearRecoveredProviderState,
extractApiKey,
isValidApiKey,
} from "@/sse/services/auth";
import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth";
import { parseImageModel, getImageProvider } from "@omniroute/open-sse/config/imageRegistry.ts";
import { errorResponse, unavailableResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
@@ -99,18 +94,11 @@ export async function POST(request: Request) {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing required field: image");
}
const apiKey = extractApiKey(request);
if (!isValidApiKey(apiKey)) {
const policyError = enforceApiKeyPolicy(apiKey);
if (policyError) {
return new Response(JSON.stringify(policyError.body), {
status: policyError.status,
headers: { "Content-Type": "application/json" },
});
}
}
const fullModel = model || "cgpt-web/gpt-5.3-instant";
const policy = await enforceApiKeyPolicy(request, fullModel);
if (policy.rejection) return policy.rejection;
const parsed = parseImageModel(fullModel);
const providerConfig = getImageProvider(parsed.provider);
if (!providerConfig) {
@@ -126,7 +114,16 @@ export async function POST(request: Request) {
);
}
const credentials = await getProviderCredentials(parsed.provider, apiKey);
const allowedConnections =
policy.apiKeyInfo?.allowedConnections && policy.apiKeyInfo.allowedConnections.length > 0
? policy.apiKeyInfo.allowedConnections
: null;
const credentials = await getProviderCredentials(
parsed.provider,
null,
allowedConnections,
fullModel
);
if (!credentials) {
return errorResponse(
HTTP_STATUS.UNAUTHORIZED,