mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
* 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>
257 lines
7.6 KiB
TypeScript
257 lines
7.6 KiB
TypeScript
"use server";
|
|
|
|
import { NextResponse } from "next/server";
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
|
import {
|
|
ensureCliConfigWriteAllowed,
|
|
getCliPrimaryConfigPath,
|
|
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";
|
|
import { getApiKeyById } from "@/lib/localDb";
|
|
|
|
// Get claude settings path based on OS
|
|
const getClaudeSettingsPath = () => getCliPrimaryConfigPath("claude");
|
|
|
|
// Read current settings
|
|
const readSettings = async () => {
|
|
try {
|
|
const settingsPath = getClaudeSettingsPath();
|
|
const content = await fs.readFile(settingsPath, "utf-8");
|
|
return JSON.parse(content);
|
|
} catch (error: any) {
|
|
if (error.code === "ENOENT") {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// GET - Check claude CLI and read current settings
|
|
export async function GET(request: Request) {
|
|
const authError = await requireCliToolsAuth(request);
|
|
if (authError) return authError;
|
|
|
|
try {
|
|
const runtime = await getCliRuntimeStatus("claude");
|
|
|
|
if (!runtime.installed || !runtime.runnable) {
|
|
return NextResponse.json({
|
|
installed: runtime.installed,
|
|
runnable: runtime.runnable,
|
|
command: runtime.command,
|
|
commandPath: runtime.commandPath,
|
|
runtimeMode: runtime.runtimeMode,
|
|
reason: runtime.reason,
|
|
settings: null,
|
|
message:
|
|
runtime.installed && !runtime.runnable
|
|
? "Claude CLI is installed but not runnable"
|
|
: "Claude CLI is not installed",
|
|
});
|
|
}
|
|
|
|
const settings = await readSettings();
|
|
const hasOmniRoute = !!settings?.env?.ANTHROPIC_BASE_URL;
|
|
|
|
return NextResponse.json({
|
|
installed: runtime.installed,
|
|
runnable: runtime.runnable,
|
|
command: runtime.command,
|
|
commandPath: runtime.commandPath,
|
|
runtimeMode: runtime.runtimeMode,
|
|
reason: runtime.reason,
|
|
settings: settings,
|
|
hasOmniRoute: hasOmniRoute,
|
|
settingsPath: getClaudeSettingsPath(),
|
|
});
|
|
} catch (error) {
|
|
console.log("Error checking claude settings:", error);
|
|
return NextResponse.json({ error: "Failed to check claude settings" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// POST - Backup old fields and write new settings
|
|
export async function POST(request: Request) {
|
|
const authError = await requireCliToolsAuth(request);
|
|
if (authError) return authError;
|
|
|
|
let rawBody;
|
|
try {
|
|
rawBody = await request.json();
|
|
} catch {
|
|
return NextResponse.json(
|
|
{
|
|
error: {
|
|
message: "Invalid request",
|
|
details: [{ field: "body", message: "Invalid JSON body" }],
|
|
},
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
try {
|
|
const writeGuard = ensureCliConfigWriteAllowed();
|
|
if (writeGuard) {
|
|
return NextResponse.json({ error: writeGuard }, { status: 403 });
|
|
}
|
|
|
|
// (#523/#526) Extract keyId BEFORE validation — Zod strips unknown fields!
|
|
// The /api/keys list endpoint returns masked key strings — sending those to
|
|
// disk would save an unusable half-hidden token. Resolving by ID guarantees
|
|
// we always write the full key value to the config file.
|
|
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
|
|
|
|
const validation = validateBody(cliSettingsEnvSchema, rawBody);
|
|
if (isValidationFailure(validation)) {
|
|
return NextResponse.json({ error: validation.error }, { status: 400 });
|
|
}
|
|
const { env } = validation.data;
|
|
|
|
// Resolve the real API key from DB by ID
|
|
if (keyId) {
|
|
try {
|
|
const keyRecord = await getApiKeyById(keyId);
|
|
if (keyRecord?.key) {
|
|
env.ANTHROPIC_AUTH_TOKEN = keyRecord.key as string;
|
|
}
|
|
} catch {
|
|
// Non-critical: fall back to whatever value was already provided in env.
|
|
}
|
|
}
|
|
|
|
const settingsPath = getClaudeSettingsPath();
|
|
const claudeDir = path.dirname(settingsPath);
|
|
|
|
// Ensure .claude directory exists
|
|
await fs.mkdir(claudeDir, { recursive: true });
|
|
|
|
// Backup current settings before modifying
|
|
await createBackup("claude", settingsPath);
|
|
|
|
// Read current settings
|
|
let currentSettings: Record<string, any> = {};
|
|
try {
|
|
const content = await fs.readFile(settingsPath, "utf-8");
|
|
currentSettings = JSON.parse(content);
|
|
} catch (error: any) {
|
|
if (error.code !== "ENOENT") {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Claude Code gateway mode expects the unified root endpoint, not a forced /v1 suffix.
|
|
if (env.ANTHROPIC_BASE_URL) {
|
|
env.ANTHROPIC_BASE_URL = normalizeClaudeBaseUrl(env.ANTHROPIC_BASE_URL);
|
|
}
|
|
|
|
// Merge new env with existing settings
|
|
const newSettings = {
|
|
...currentSettings,
|
|
env: {
|
|
...(currentSettings.env || {}),
|
|
...env,
|
|
},
|
|
};
|
|
|
|
// Write new settings
|
|
await fs.writeFile(settingsPath, JSON.stringify(newSettings, null, 2));
|
|
|
|
// Persist last-configured timestamp
|
|
try {
|
|
saveCliToolLastConfigured("claude");
|
|
} catch {
|
|
/* non-critical */
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: "Settings updated successfully",
|
|
});
|
|
} catch (error) {
|
|
console.log("Error updating claude settings:", error);
|
|
return NextResponse.json({ error: "Failed to update claude settings" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// Fields to remove when resetting
|
|
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",
|
|
"API_TIMEOUT_MS",
|
|
];
|
|
|
|
// DELETE - Reset settings (remove env fields)
|
|
export async function DELETE(request: Request) {
|
|
const authError = await requireCliToolsAuth(request);
|
|
if (authError) return authError;
|
|
|
|
try {
|
|
const writeGuard = ensureCliConfigWriteAllowed();
|
|
if (writeGuard) {
|
|
return NextResponse.json({ error: writeGuard }, { status: 403 });
|
|
}
|
|
|
|
const settingsPath = getClaudeSettingsPath();
|
|
|
|
// Read current settings
|
|
let currentSettings: Record<string, any> = {};
|
|
try {
|
|
const content = await fs.readFile(settingsPath, "utf-8");
|
|
currentSettings = JSON.parse(content);
|
|
} catch (error: any) {
|
|
if (error.code === "ENOENT") {
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: "No settings file to reset",
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
// Backup current settings before resetting
|
|
await createBackup("claude", settingsPath);
|
|
|
|
// Remove specified env fields
|
|
if (currentSettings.env) {
|
|
RESET_ENV_KEYS.forEach((key) => {
|
|
delete currentSettings.env[key];
|
|
});
|
|
|
|
// Clean up empty env object
|
|
if (Object.keys(currentSettings.env).length === 0) {
|
|
delete currentSettings.env;
|
|
}
|
|
}
|
|
|
|
// Write updated settings
|
|
await fs.writeFile(settingsPath, JSON.stringify(currentSettings, null, 2));
|
|
|
|
// Clear last-configured timestamp
|
|
try {
|
|
deleteCliToolLastConfigured("claude");
|
|
} catch {
|
|
/* non-critical */
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: "Settings reset successfully",
|
|
});
|
|
} catch (error) {
|
|
console.log("Error resetting claude settings:", error);
|
|
return NextResponse.json({ error: "Failed to reset claude settings" }, { status: 500 });
|
|
}
|
|
}
|