Initial commit
This commit is contained in:
187
src/app/api/cli-tools/claude-settings/route.js
Normal file
187
src/app/api/cli-tools/claude-settings/route.js
Normal file
@@ -0,0 +1,187 @@
|
||||
"use server";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Get claude settings path based on OS
|
||||
const getClaudeSettingsPath = () => {
|
||||
const homeDir = os.homedir();
|
||||
return path.join(homeDir, ".claude", "settings.json");
|
||||
};
|
||||
|
||||
|
||||
// Check if claude CLI is installed
|
||||
const checkClaudeInstalled = async () => {
|
||||
try {
|
||||
const isWindows = os.platform() === "win32";
|
||||
const command = isWindows ? "where claude" : "which claude";
|
||||
await execAsync(command);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Read current settings
|
||||
const readSettings = async () => {
|
||||
try {
|
||||
const settingsPath = getClaudeSettingsPath();
|
||||
const content = await fs.readFile(settingsPath, "utf-8");
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// GET - Check claude CLI and read current settings
|
||||
export async function GET() {
|
||||
try {
|
||||
const isInstalled = await checkClaudeInstalled();
|
||||
|
||||
if (!isInstalled) {
|
||||
return NextResponse.json({
|
||||
installed: false,
|
||||
settings: null,
|
||||
message: "Claude CLI is not installed",
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await readSettings();
|
||||
const has9Router = !!(settings?.env?.ANTHROPIC_BASE_URL);
|
||||
|
||||
return NextResponse.json({
|
||||
installed: true,
|
||||
settings: settings,
|
||||
has9Router: has9Router,
|
||||
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) {
|
||||
try {
|
||||
const { env } = await request.json();
|
||||
|
||||
if (!env || typeof env !== "object") {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid env object" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const settingsPath = getClaudeSettingsPath();
|
||||
const claudeDir = path.dirname(settingsPath);
|
||||
|
||||
// Ensure .claude directory exists
|
||||
await fs.mkdir(claudeDir, { recursive: true });
|
||||
|
||||
// Read current settings
|
||||
let currentSettings = {};
|
||||
try {
|
||||
const content = await fs.readFile(settingsPath, "utf-8");
|
||||
currentSettings = JSON.parse(content);
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
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_DEFAULT_OPUS_MODEL",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
||||
"API_TIMEOUT_MS",
|
||||
];
|
||||
|
||||
// DELETE - Reset settings (remove env fields)
|
||||
export async function DELETE() {
|
||||
try {
|
||||
const settingsPath = getClaudeSettingsPath();
|
||||
|
||||
// Read current settings
|
||||
let currentSettings = {};
|
||||
try {
|
||||
const content = await fs.readFile(settingsPath, "utf-8");
|
||||
currentSettings = JSON.parse(content);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "No settings file to reset",
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
246
src/app/api/cli-tools/codex-settings/route.js
Normal file
246
src/app/api/cli-tools/codex-settings/route.js
Normal file
@@ -0,0 +1,246 @@
|
||||
"use server";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const getCodexDir = () => path.join(os.homedir(), ".codex");
|
||||
const getCodexConfigPath = () => path.join(getCodexDir(), "config.toml");
|
||||
const getCodexAuthPath = () => path.join(getCodexDir(), "auth.json");
|
||||
|
||||
// Parse TOML config to object (simple parser for codex config)
|
||||
const parseToml = (content) => {
|
||||
const result = { _root: {}, _sections: {} };
|
||||
let currentSection = "_root";
|
||||
|
||||
content.split("\n").forEach((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) return;
|
||||
|
||||
// Section header like [model_providers.9router]
|
||||
const sectionMatch = trimmed.match(/^\[(.+)\]$/);
|
||||
if (sectionMatch) {
|
||||
currentSection = sectionMatch[1];
|
||||
result._sections[currentSection] = {};
|
||||
return;
|
||||
}
|
||||
|
||||
// Key = value
|
||||
const kvMatch = trimmed.match(/^([^=]+)\s*=\s*(.+)$/);
|
||||
if (kvMatch) {
|
||||
const key = kvMatch[1].trim();
|
||||
let value = kvMatch[2].trim();
|
||||
// Remove quotes
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (currentSection === "_root") {
|
||||
result._root[key] = value;
|
||||
} else {
|
||||
result._sections[currentSection][key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Convert parsed object back to TOML string
|
||||
const toToml = (parsed) => {
|
||||
let lines = [];
|
||||
|
||||
// Root level keys
|
||||
Object.entries(parsed._root).forEach(([key, value]) => {
|
||||
lines.push(`${key} = "${value}"`);
|
||||
});
|
||||
|
||||
// Sections
|
||||
Object.entries(parsed._sections).forEach(([section, values]) => {
|
||||
lines.push("");
|
||||
lines.push(`[${section}]`);
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
lines.push(`${key} = "${value}"`);
|
||||
});
|
||||
});
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
};
|
||||
|
||||
// Check if codex CLI is installed
|
||||
const checkCodexInstalled = async () => {
|
||||
try {
|
||||
const isWindows = os.platform() === "win32";
|
||||
const command = isWindows ? "where codex" : "which codex";
|
||||
await execAsync(command);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Read current config.toml
|
||||
const readConfig = async () => {
|
||||
try {
|
||||
const configPath = getCodexConfigPath();
|
||||
const content = await fs.readFile(configPath, "utf-8");
|
||||
return content;
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") return null;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Check if config has 9Router settings
|
||||
const has9RouterConfig = (config) => {
|
||||
if (!config) return false;
|
||||
return config.includes("model_provider = \"9router\"") || config.includes("[model_providers.9router]");
|
||||
};
|
||||
|
||||
// GET - Check codex CLI and read current settings
|
||||
export async function GET() {
|
||||
try {
|
||||
const isInstalled = await checkCodexInstalled();
|
||||
|
||||
if (!isInstalled) {
|
||||
return NextResponse.json({
|
||||
installed: false,
|
||||
config: null,
|
||||
message: "Codex CLI is not installed",
|
||||
});
|
||||
}
|
||||
|
||||
const config = await readConfig();
|
||||
|
||||
return NextResponse.json({
|
||||
installed: true,
|
||||
config,
|
||||
has9Router: has9RouterConfig(config),
|
||||
configPath: getCodexConfigPath(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error checking codex settings:", error);
|
||||
return NextResponse.json({ error: "Failed to check codex settings" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST - Update 9Router settings (merge with existing config)
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const { baseUrl, apiKey, model } = await request.json();
|
||||
|
||||
if (!baseUrl || !apiKey || !model) {
|
||||
return NextResponse.json({ error: "baseUrl, apiKey and model are required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const codexDir = getCodexDir();
|
||||
const configPath = getCodexConfigPath();
|
||||
|
||||
// Ensure directory exists
|
||||
await fs.mkdir(codexDir, { recursive: true });
|
||||
|
||||
// Read and parse existing config
|
||||
let parsed = { _root: {}, _sections: {} };
|
||||
try {
|
||||
const existingConfig = await fs.readFile(configPath, "utf-8");
|
||||
parsed = parseToml(existingConfig);
|
||||
} catch { /* No existing config */ }
|
||||
|
||||
// Update only 9Router related fields (api_key goes to auth.json, not config.toml)
|
||||
parsed._root.model = model;
|
||||
parsed._root.model_provider = "9router";
|
||||
|
||||
// Update or create 9router provider section (no api_key - Codex reads from auth.json)
|
||||
parsed._sections["model_providers.9router"] = {
|
||||
name: "9Router",
|
||||
base_url: `${baseUrl}/v1`,
|
||||
wire_api: "responses",
|
||||
};
|
||||
|
||||
// Write merged config
|
||||
const configContent = toToml(parsed);
|
||||
await fs.writeFile(configPath, configContent);
|
||||
|
||||
// Update auth.json with OPENAI_API_KEY (Codex reads this first)
|
||||
const authPath = getCodexAuthPath();
|
||||
let authData = {};
|
||||
try {
|
||||
const existingAuth = await fs.readFile(authPath, "utf-8");
|
||||
authData = JSON.parse(existingAuth);
|
||||
} catch { /* No existing auth */ }
|
||||
|
||||
authData.OPENAI_API_KEY = apiKey;
|
||||
await fs.writeFile(authPath, JSON.stringify(authData, null, 2));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Codex settings applied successfully!",
|
||||
configPath,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error updating codex settings:", error);
|
||||
return NextResponse.json({ error: "Failed to update codex settings" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - Remove 9Router settings only (keep other settings)
|
||||
export async function DELETE() {
|
||||
try {
|
||||
const configPath = getCodexConfigPath();
|
||||
|
||||
// Read and parse existing config
|
||||
let parsed = { _root: {}, _sections: {} };
|
||||
try {
|
||||
const existingConfig = await fs.readFile(configPath, "utf-8");
|
||||
parsed = parseToml(existingConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "No config file to reset",
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Remove 9Router related root fields only if they point to 9router
|
||||
if (parsed._root.model_provider === "9router") {
|
||||
delete parsed._root.model;
|
||||
delete parsed._root.model_provider;
|
||||
}
|
||||
|
||||
// Remove 9router provider section
|
||||
delete parsed._sections["model_providers.9router"];
|
||||
|
||||
// Write updated config
|
||||
const configContent = toToml(parsed);
|
||||
await fs.writeFile(configPath, configContent);
|
||||
|
||||
// Remove OPENAI_API_KEY from auth.json
|
||||
const authPath = getCodexAuthPath();
|
||||
try {
|
||||
const existingAuth = await fs.readFile(authPath, "utf-8");
|
||||
const authData = JSON.parse(existingAuth);
|
||||
delete authData.OPENAI_API_KEY;
|
||||
|
||||
// Write back or delete if empty
|
||||
if (Object.keys(authData).length === 0) {
|
||||
await fs.unlink(authPath);
|
||||
} else {
|
||||
await fs.writeFile(authPath, JSON.stringify(authData, null, 2));
|
||||
}
|
||||
} catch { /* No auth file */ }
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "9Router settings removed successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error resetting codex settings:", error);
|
||||
return NextResponse.json({ error: "Failed to reset codex settings" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
50
src/app/api/cloud/auth/route.js
Normal file
50
src/app/api/cloud/auth/route.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { validateApiKey, getProviderConnections, getModelAliases } from "@/models";
|
||||
|
||||
// Verify API key and return provider credentials
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
// return NextResponse.json({ error: "Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const apiKey = authHeader.slice(7);
|
||||
|
||||
// Validate API key
|
||||
const isValid = await validateApiKey(apiKey);
|
||||
if (!isValid) {
|
||||
// return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get active provider connections
|
||||
const connections = await getProviderConnections({ isActive: true });
|
||||
|
||||
// Map connections
|
||||
const mappedConnections = connections.map(conn => ({
|
||||
provider: conn.provider,
|
||||
authType: conn.authType,
|
||||
apiKey: conn.apiKey || null,
|
||||
accessToken: conn.accessToken || null,
|
||||
refreshToken: conn.refreshToken || null,
|
||||
projectId: conn.projectId || null,
|
||||
expiresAt: conn.expiresAt,
|
||||
priority: conn.priority,
|
||||
globalPriority: conn.globalPriority,
|
||||
defaultModel: conn.defaultModel,
|
||||
isActive: conn.isActive
|
||||
}));
|
||||
|
||||
// Get model aliases
|
||||
const modelAliases = await getModelAliases();
|
||||
|
||||
return NextResponse.json({
|
||||
connections: mappedConnections,
|
||||
modelAliases
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log("Cloud auth error:", error);
|
||||
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
57
src/app/api/cloud/credentials/update/route.js
Normal file
57
src/app/api/cloud/credentials/update/route.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { validateApiKey, getProviderConnections, updateProviderConnection } from "@/models";
|
||||
|
||||
// Update provider credentials (for cloud token refresh)
|
||||
export async function PUT(request) {
|
||||
try {
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const apiKey = authHeader.slice(7);
|
||||
const body = await request.json();
|
||||
const { provider, credentials } = body;
|
||||
|
||||
if (!provider || !credentials) {
|
||||
return NextResponse.json({ error: "Provider and credentials required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate API key
|
||||
const isValid = await validateApiKey(apiKey);
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Find active connection for provider
|
||||
const connections = await getProviderConnections({ provider, isActive: true });
|
||||
const connection = connections[0];
|
||||
|
||||
if (!connection) {
|
||||
return NextResponse.json({ error: `No active connection found for provider: ${provider}` }, { status: 404 });
|
||||
}
|
||||
|
||||
// Update credentials
|
||||
const updateData = {};
|
||||
if (credentials.accessToken) {
|
||||
updateData.accessToken = credentials.accessToken;
|
||||
}
|
||||
if (credentials.refreshToken) {
|
||||
updateData.refreshToken = credentials.refreshToken;
|
||||
}
|
||||
if (credentials.expiresIn) {
|
||||
updateData.expiresAt = new Date(Date.now() + credentials.expiresIn * 1000).toISOString();
|
||||
}
|
||||
|
||||
await updateProviderConnection(connection.id, updateData);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Credentials updated for provider: ${provider}`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log("Update credentials error:", error);
|
||||
return NextResponse.json({ error: "Failed to update credentials" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
50
src/app/api/cloud/model/resolve/route.js
Normal file
50
src/app/api/cloud/model/resolve/route.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { validateApiKey, getModelAliases } from "@/models";
|
||||
|
||||
// Resolve model alias to provider/model
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const apiKey = authHeader.slice(7);
|
||||
|
||||
const body = await request.json();
|
||||
const { alias } = body;
|
||||
|
||||
if (!alias) {
|
||||
return NextResponse.json({ error: "Missing alias" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate API key
|
||||
const isValid = await validateApiKey(apiKey);
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get model aliases
|
||||
const modelAliases = await getModelAliases();
|
||||
const resolved = modelAliases[alias];
|
||||
|
||||
if (resolved) {
|
||||
// Parse provider/model
|
||||
const firstSlash = resolved.indexOf("/");
|
||||
if (firstSlash > 0) {
|
||||
return NextResponse.json({
|
||||
alias,
|
||||
provider: resolved.slice(0, firstSlash),
|
||||
model: resolved.slice(firstSlash + 1)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Not found
|
||||
return NextResponse.json({ error: "Alias not found" }, { status: 404 });
|
||||
|
||||
} catch (error) {
|
||||
console.log("Model resolve error:", error);
|
||||
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
92
src/app/api/cloud/models/alias/route.js
Normal file
92
src/app/api/cloud/models/alias/route.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { validateApiKey, getModelAliases, setModelAlias, isCloudEnabled } from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
// PUT /api/cloud/models/alias - Set model alias (for cloud/CLI)
|
||||
export async function PUT(request) {
|
||||
try {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
const apiKey = authHeader?.replace("Bearer ", "");
|
||||
|
||||
if (!apiKey) {
|
||||
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const isValid = await validateApiKey(apiKey);
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { model, alias } = body;
|
||||
|
||||
if (!model || !alias) {
|
||||
return NextResponse.json({ error: "Model and alias required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if alias already exists for different model
|
||||
const aliases = await getModelAliases();
|
||||
const existingModel = aliases[alias];
|
||||
if (existingModel && existingModel !== model) {
|
||||
return NextResponse.json({
|
||||
error: `Alias '${alias}' already in use for model '${existingModel}'`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Update alias
|
||||
await setModelAlias(alias, model);
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
model,
|
||||
alias,
|
||||
message: `Alias '${alias}' set for model '${model}'`
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error updating alias:", error);
|
||||
return NextResponse.json({ error: "Failed to update alias" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing aliases to cloud:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/cloud/models/alias - Get all aliases
|
||||
export async function GET(request) {
|
||||
try {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
const apiKey = authHeader?.replace("Bearer ", "");
|
||||
|
||||
if (!apiKey) {
|
||||
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const isValid = await validateApiKey(apiKey);
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const aliases = await getModelAliases();
|
||||
|
||||
return NextResponse.json({ aliases });
|
||||
} catch (error) {
|
||||
console.log("Error fetching aliases:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch aliases" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
94
src/app/api/combos/[id]/route.js
Normal file
94
src/app/api/combos/[id]/route.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getComboById, updateCombo, deleteCombo, getComboByName, isCloudEnabled } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
// Validate combo name: only a-z, A-Z, 0-9, -, _
|
||||
const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
// GET /api/combos/[id] - Get combo by ID
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const combo = await getComboById(id);
|
||||
|
||||
if (!combo) {
|
||||
return NextResponse.json({ error: "Combo not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(combo);
|
||||
} catch (error) {
|
||||
console.log("Error fetching combo:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch combo" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/combos/[id] - Update combo
|
||||
export async function PUT(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
// Validate name format if provided
|
||||
if (body.name) {
|
||||
if (!VALID_NAME_REGEX.test(body.name)) {
|
||||
return NextResponse.json({ error: "Name can only contain letters, numbers, - and _" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if name already exists (exclude current combo)
|
||||
const existing = await getComboByName(body.name);
|
||||
if (existing && existing.id !== id) {
|
||||
return NextResponse.json({ error: "Combo name already exists" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const combo = await updateCombo(id, body);
|
||||
|
||||
if (!combo) {
|
||||
return NextResponse.json({ error: "Combo not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json(combo);
|
||||
} catch (error) {
|
||||
console.log("Error updating combo:", error);
|
||||
return NextResponse.json({ error: "Failed to update combo" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/combos/[id] - Delete combo
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const success = await deleteCombo(id);
|
||||
|
||||
if (!success) {
|
||||
return NextResponse.json({ error: "Combo not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.log("Error deleting combo:", error);
|
||||
return NextResponse.json({ error: "Failed to delete combo" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing to cloud:", error);
|
||||
}
|
||||
}
|
||||
66
src/app/api/combos/route.js
Normal file
66
src/app/api/combos/route.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCombos, createCombo, getComboByName, isCloudEnabled } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
// Validate combo name: only a-z, A-Z, 0-9, -, _
|
||||
const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
// GET /api/combos - Get all combos
|
||||
export async function GET() {
|
||||
try {
|
||||
const combos = await getCombos();
|
||||
return NextResponse.json({ combos });
|
||||
} catch (error) {
|
||||
console.log("Error fetching combos:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch combos" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/combos - Create new combo
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, models } = body;
|
||||
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: "Name is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate name format
|
||||
if (!VALID_NAME_REGEX.test(name)) {
|
||||
return NextResponse.json({ error: "Name can only contain letters, numbers, - and _" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if name already exists
|
||||
const existing = await getComboByName(name);
|
||||
if (existing) {
|
||||
return NextResponse.json({ error: "Combo name already exists" }, { status: 400 });
|
||||
}
|
||||
|
||||
const combo = await createCombo({ name, models: models || [] });
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json(combo, { status: 201 });
|
||||
} catch (error) {
|
||||
console.log("Error creating combo:", error);
|
||||
return NextResponse.json({ error: "Failed to create combo" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing to cloud:", error);
|
||||
}
|
||||
}
|
||||
7
src/app/api/init/route.js
Normal file
7
src/app/api/init/route.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// Auto-initialize cloud sync when server starts
|
||||
import "@/lib/initCloudSync";
|
||||
|
||||
// This API route is called automatically to initialize sync
|
||||
export async function GET() {
|
||||
return new Response("Initialized", { status: 200 });
|
||||
}
|
||||
39
src/app/api/keys/[id]/route.js
Normal file
39
src/app/api/keys/[id]/route.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { deleteApiKey, isCloudEnabled } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
// DELETE /api/keys/[id] - Delete API key
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
const deleted = await deleteApiKey(id);
|
||||
if (!deleted) {
|
||||
return NextResponse.json({ error: "Key not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncKeysToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ message: "Key deleted successfully" });
|
||||
} catch (error) {
|
||||
console.log("Error deleting key:", error);
|
||||
return NextResponse.json({ error: "Failed to delete key" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync API keys to Cloud if enabled
|
||||
*/
|
||||
async function syncKeysToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing keys to cloud:", error);
|
||||
}
|
||||
}
|
||||
59
src/app/api/keys/route.js
Normal file
59
src/app/api/keys/route.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeys, createApiKey, isCloudEnabled } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
// GET /api/keys - List API keys
|
||||
export async function GET() {
|
||||
try {
|
||||
const keys = await getApiKeys();
|
||||
return NextResponse.json({ keys });
|
||||
} catch (error) {
|
||||
console.log("Error fetching keys:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch keys" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/keys - Create new API key
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name } = body;
|
||||
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: "Name is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Always get machineId from server
|
||||
const machineId = await getConsistentMachineId();
|
||||
const apiKey = await createApiKey(name, machineId);
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncKeysToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
key: apiKey.key,
|
||||
name: apiKey.name,
|
||||
id: apiKey.id,
|
||||
machineId: apiKey.machineId,
|
||||
}, { status: 201 });
|
||||
} catch (error) {
|
||||
console.log("Error creating key:", error);
|
||||
return NextResponse.json({ error: "Failed to create key" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync API keys to Cloud if enabled
|
||||
*/
|
||||
async function syncKeysToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing keys to cloud:", error);
|
||||
}
|
||||
}
|
||||
83
src/app/api/models/alias/route.js
Normal file
83
src/app/api/models/alias/route.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getModelAliases, setModelAlias, deleteModelAlias, isCloudEnabled } from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
// GET /api/models/alias - Get all aliases
|
||||
export async function GET() {
|
||||
try {
|
||||
const aliases = await getModelAliases();
|
||||
return NextResponse.json({ aliases });
|
||||
} catch (error) {
|
||||
console.log("Error fetching aliases:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch aliases" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/models/alias - Set model alias
|
||||
export async function PUT(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { model, alias } = body;
|
||||
|
||||
if (!model || !alias) {
|
||||
return NextResponse.json({ error: "Model and alias required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const aliases = await getModelAliases();
|
||||
|
||||
// Check if alias already used by different model
|
||||
const existingModel = aliases[alias];
|
||||
if (existingModel && existingModel !== model) {
|
||||
return NextResponse.json({
|
||||
error: `Alias '${alias}' already in use for model '${existingModel}'`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Delete old alias for this model (if exists and different from new alias)
|
||||
const oldAlias = Object.entries(aliases).find(([a, m]) => m === model && a !== alias)?.[0];
|
||||
if (oldAlias) {
|
||||
await deleteModelAlias(oldAlias);
|
||||
}
|
||||
|
||||
await setModelAlias(alias, model);
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ success: true, model, alias });
|
||||
} catch (error) {
|
||||
console.log("Error updating alias:", error);
|
||||
return NextResponse.json({ error: "Failed to update alias" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/models/alias?alias=xxx - Delete alias
|
||||
export async function DELETE(request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const alias = searchParams.get("alias");
|
||||
|
||||
if (!alias) {
|
||||
return NextResponse.json({ error: "Alias required" }, { status: 400 });
|
||||
}
|
||||
|
||||
await deleteModelAlias(alias);
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.log("Error deleting alias:", error);
|
||||
return NextResponse.json({ error: "Failed to delete alias" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing aliases to cloud:", error);
|
||||
}
|
||||
}
|
||||
55
src/app/api/models/route.js
Normal file
55
src/app/api/models/route.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getModelAliases, setModelAlias } from "@/models";
|
||||
import { AI_MODELS } from "@/shared/constants/config";
|
||||
|
||||
// GET /api/models - Get models with aliases
|
||||
export async function GET() {
|
||||
try {
|
||||
const modelAliases = await getModelAliases();
|
||||
|
||||
const models = AI_MODELS.map((m) => {
|
||||
const fullModel = `${m.provider}/${m.model}`;
|
||||
return {
|
||||
...m,
|
||||
fullModel,
|
||||
alias: modelAliases[fullModel] || m.model,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ models });
|
||||
} catch (error) {
|
||||
console.log("Error fetching models:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch models" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/models - Update model alias
|
||||
export async function PUT(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { model, alias } = body;
|
||||
|
||||
if (!model || !alias) {
|
||||
return NextResponse.json({ error: "Model and alias required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const modelAliases = await getModelAliases();
|
||||
|
||||
// Check if alias already exists for different model
|
||||
const existingModel = Object.entries(modelAliases).find(
|
||||
([key, val]) => val === alias && key !== model
|
||||
);
|
||||
|
||||
if (existingModel) {
|
||||
return NextResponse.json({ error: "Alias already in use" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Update alias
|
||||
await setModelAlias(model, alias);
|
||||
|
||||
return NextResponse.json({ success: true, model, alias });
|
||||
} catch (error) {
|
||||
console.log("Error updating alias:", error);
|
||||
return NextResponse.json({ error: "Failed to update alias" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
187
src/app/api/oauth/[provider]/[action]/route.js
Normal file
187
src/app/api/oauth/[provider]/[action]/route.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
getProvider,
|
||||
generateAuthData,
|
||||
exchangeTokens,
|
||||
requestDeviceCode,
|
||||
pollForToken
|
||||
} from "@/lib/oauth/providers";
|
||||
import { createProviderConnection, isCloudEnabled } from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
/**
|
||||
* Dynamic OAuth API Route
|
||||
* Handles: authorize, exchange, device-code, poll
|
||||
*/
|
||||
|
||||
// GET /api/oauth/[provider]/authorize - Generate auth URL
|
||||
// GET /api/oauth/[provider]/device-code - Request device code (for device_code flow)
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { provider, action } = await params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
if (action === "authorize") {
|
||||
const redirectUri = searchParams.get("redirect_uri") || "http://localhost:8080/callback";
|
||||
const authData = generateAuthData(provider, redirectUri);
|
||||
return NextResponse.json(authData);
|
||||
}
|
||||
|
||||
if (action === "device-code") {
|
||||
const providerData = getProvider(provider);
|
||||
if (providerData.flowType !== "device_code") {
|
||||
return NextResponse.json({ error: "Provider does not support device code flow" }, { status: 400 });
|
||||
}
|
||||
|
||||
const authData = generateAuthData(provider, null);
|
||||
|
||||
// For providers that don't use PKCE (like GitHub), don't pass codeChallenge
|
||||
let deviceData;
|
||||
if (provider === "github") {
|
||||
deviceData = await requestDeviceCode(provider);
|
||||
} else {
|
||||
// Qwen and other providers use PKCE
|
||||
deviceData = await requestDeviceCode(provider, authData.codeChallenge);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
...deviceData,
|
||||
codeVerifier: authData.codeVerifier,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.log("OAuth GET error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/oauth/[provider]/exchange - Exchange code for tokens and save
|
||||
// POST /api/oauth/[provider]/poll - Poll for token (device_code flow)
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { provider, action } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
if (action === "exchange") {
|
||||
const { code, redirectUri, codeVerifier, state } = body;
|
||||
|
||||
if (!code || !redirectUri || !codeVerifier) {
|
||||
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Exchange code for tokens
|
||||
const tokenData = await exchangeTokens(provider, code, redirectUri, codeVerifier, state);
|
||||
|
||||
// Save to database
|
||||
const connection = await createProviderConnection({
|
||||
provider,
|
||||
authType: "oauth",
|
||||
...tokenData,
|
||||
expiresAt: tokenData.expiresIn
|
||||
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
|
||||
: null,
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
connection: {
|
||||
id: connection.id,
|
||||
provider: connection.provider,
|
||||
email: connection.email,
|
||||
displayName: connection.displayName,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (action === "poll") {
|
||||
const { deviceCode, codeVerifier } = body;
|
||||
|
||||
if (!deviceCode) {
|
||||
return NextResponse.json({ error: "Missing device code" }, { status: 400 });
|
||||
}
|
||||
|
||||
// For providers that don't use PKCE (like GitHub), don't pass codeVerifier
|
||||
let result;
|
||||
if (provider === "github") {
|
||||
result = await pollForToken(provider, deviceCode);
|
||||
} else {
|
||||
// Qwen and other providers use PKCE
|
||||
if (!codeVerifier) {
|
||||
return NextResponse.json({ error: "Missing code verifier" }, { status: 400 });
|
||||
}
|
||||
result = await pollForToken(provider, deviceCode, codeVerifier);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
// Save to database
|
||||
const connection = await createProviderConnection({
|
||||
provider,
|
||||
authType: "oauth",
|
||||
...result.tokens,
|
||||
expiresAt: result.tokens.expiresIn
|
||||
? new Date(Date.now() + result.tokens.expiresIn * 1000).toISOString()
|
||||
: null,
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
connection: {
|
||||
id: connection.id,
|
||||
provider: connection.provider,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Still pending or error
|
||||
if (!result.pending) {
|
||||
// Save error to database for actual errors (not pending)
|
||||
await createProviderConnection({
|
||||
provider,
|
||||
authType: "oauth",
|
||||
testStatus: "error",
|
||||
lastError: result.errorDescription,
|
||||
errorCode: result.error,
|
||||
lastErrorAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: result.error,
|
||||
errorDescription: result.errorDescription,
|
||||
pending: result.pending || result.error === "authorization_pending",
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.log("OAuth POST error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing to cloud after OAuth:", error);
|
||||
}
|
||||
}
|
||||
148
src/app/api/providers/[id]/models/route.js
Normal file
148
src/app/api/providers/[id]/models/route.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnectionById } from "@/models";
|
||||
|
||||
// Provider models endpoints configuration
|
||||
const PROVIDER_MODELS_CONFIG = {
|
||||
claude: {
|
||||
url: "https://api.anthropic.com/v1/models",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
authHeader: "x-api-key",
|
||||
parseResponse: (data) => data.data || []
|
||||
},
|
||||
gemini: {
|
||||
url: "https://generativelanguage.googleapis.com/v1beta/models",
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
authQuery: "key", // Use query param for API key
|
||||
parseResponse: (data) => data.models || []
|
||||
},
|
||||
"gemini-cli": {
|
||||
url: "https://generativelanguage.googleapis.com/v1beta/models",
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.models || []
|
||||
},
|
||||
qwen: {
|
||||
url: "https://portal.qwen.ai/v1/models",
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.data || []
|
||||
},
|
||||
antigravity: {
|
||||
url: "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:models",
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
body: {},
|
||||
parseResponse: (data) => data.models || []
|
||||
},
|
||||
openai: {
|
||||
url: "https://api.openai.com/v1/models",
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.data || []
|
||||
},
|
||||
openrouter: {
|
||||
url: "https://openrouter.ai/api/v1/models",
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.data || []
|
||||
},
|
||||
anthropic: {
|
||||
url: "https://api.anthropic.com/v1/models",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
authHeader: "x-api-key",
|
||||
parseResponse: (data) => data.data || []
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /api/providers/[id]/models - Get models list from provider
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const connection = await getProviderConnectionById(id);
|
||||
|
||||
if (!connection) {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const config = PROVIDER_MODELS_CONFIG[connection.provider];
|
||||
if (!config) {
|
||||
return NextResponse.json(
|
||||
{ error: `Provider ${connection.provider} does not support models listing` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get auth token
|
||||
const token = connection.accessToken || connection.apiKey;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "No valid token found" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Build request URL
|
||||
let url = config.url;
|
||||
if (config.authQuery) {
|
||||
url += `?${config.authQuery}=${token}`;
|
||||
}
|
||||
|
||||
// Build headers
|
||||
const headers = { ...config.headers };
|
||||
if (config.authHeader && !config.authQuery) {
|
||||
headers[config.authHeader] = (config.authPrefix || "") + token;
|
||||
}
|
||||
|
||||
// Make request
|
||||
const fetchOptions = {
|
||||
method: config.method,
|
||||
headers
|
||||
};
|
||||
|
||||
if (config.body && config.method === "POST") {
|
||||
fetchOptions.body = JSON.stringify(config.body);
|
||||
}
|
||||
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.log(`Error fetching models from ${connection.provider}:`, errorText);
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to fetch models: ${response.status}` },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const models = config.parseResponse(data);
|
||||
|
||||
return NextResponse.json({
|
||||
provider: connection.provider,
|
||||
connectionId: connection.id,
|
||||
models
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error fetching provider models:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch models" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
102
src/app/api/providers/[id]/route.js
Normal file
102
src/app/api/providers/[id]/route.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnectionById, updateProviderConnection, deleteProviderConnection, isCloudEnabled } from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
// GET /api/providers/[id] - Get single connection
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const connection = await getProviderConnectionById(id);
|
||||
|
||||
if (!connection) {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Hide sensitive fields
|
||||
const result = { ...connection };
|
||||
delete result.apiKey;
|
||||
delete result.accessToken;
|
||||
delete result.refreshToken;
|
||||
delete result.idToken;
|
||||
|
||||
return NextResponse.json({ connection: result });
|
||||
} catch (error) {
|
||||
console.log("Error fetching connection:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch connection" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/providers/[id] - Update connection
|
||||
export async function PUT(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { name, priority, globalPriority, defaultModel, isActive, apiKey } = body;
|
||||
|
||||
const existing = await getProviderConnectionById(id);
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (priority !== undefined) updateData.priority = priority;
|
||||
if (globalPriority !== undefined) updateData.globalPriority = globalPriority;
|
||||
if (defaultModel !== undefined) updateData.defaultModel = defaultModel;
|
||||
if (isActive !== undefined) updateData.isActive = isActive;
|
||||
if (apiKey && existing.authType === "apikey") updateData.apiKey = apiKey;
|
||||
|
||||
const updated = await updateProviderConnection(id, updateData);
|
||||
|
||||
// Hide sensitive fields
|
||||
const result = { ...updated };
|
||||
delete result.apiKey;
|
||||
delete result.accessToken;
|
||||
delete result.refreshToken;
|
||||
delete result.idToken;
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ connection: result });
|
||||
} catch (error) {
|
||||
console.log("Error updating connection:", error);
|
||||
return NextResponse.json({ error: "Failed to update connection" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/providers/[id] - Delete connection
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
const deleted = await deleteProviderConnection(id);
|
||||
if (!deleted) {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ message: "Connection deleted successfully" });
|
||||
} catch (error) {
|
||||
console.log("Error deleting connection:", error);
|
||||
return NextResponse.json({ error: "Failed to delete connection" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing providers to cloud:", error);
|
||||
}
|
||||
}
|
||||
95
src/app/api/providers/[id]/test/route.js
Normal file
95
src/app/api/providers/[id]/test/route.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb";
|
||||
|
||||
// POST /api/providers/[id]/test - Test connection
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const connection = await getProviderConnectionById(id);
|
||||
|
||||
if (!connection) {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
let isValid = false;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
if (connection.authType === "apikey") {
|
||||
// Test API key
|
||||
switch (connection.provider) {
|
||||
case "openai":
|
||||
const openaiRes = await fetch("https://api.openai.com/v1/models", {
|
||||
headers: { "Authorization": `Bearer ${connection.apiKey}` },
|
||||
});
|
||||
isValid = openaiRes.ok;
|
||||
break;
|
||||
|
||||
case "anthropic":
|
||||
const anthropicRes = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-api-key": connection.apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "claude-3-haiku-20240307",
|
||||
max_tokens: 1,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
}),
|
||||
});
|
||||
isValid = anthropicRes.status !== 401;
|
||||
break;
|
||||
|
||||
case "gemini":
|
||||
const geminiRes = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${connection.apiKey}`);
|
||||
isValid = geminiRes.ok;
|
||||
break;
|
||||
|
||||
case "openrouter":
|
||||
const openrouterRes = await fetch("https://openrouter.ai/api/v1/models", {
|
||||
headers: { "Authorization": `Bearer ${connection.apiKey}` },
|
||||
});
|
||||
isValid = openrouterRes.ok;
|
||||
break;
|
||||
|
||||
default:
|
||||
error = "Provider test not supported";
|
||||
}
|
||||
} else {
|
||||
// OAuth - check if token exists and not expired
|
||||
if (connection.accessToken) {
|
||||
if (connection.expiresAt) {
|
||||
const expiresAt = new Date(connection.expiresAt).getTime();
|
||||
isValid = expiresAt > Date.now();
|
||||
if (!isValid) error = "Token expired";
|
||||
} else {
|
||||
isValid = true;
|
||||
}
|
||||
} else {
|
||||
error = "No access token";
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Update status in db
|
||||
await updateProviderConnection(id, {
|
||||
testStatus: isValid ? "active" : "error",
|
||||
lastError: isValid ? null : error,
|
||||
lastErrorAt: isValid ? null : new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
valid: isValid,
|
||||
error: isValid ? null : error,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error testing connection:", error);
|
||||
return NextResponse.json({ error: "Test failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
20
src/app/api/providers/client/route.js
Normal file
20
src/app/api/providers/client/route.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnections } from "@/lib/localDb";
|
||||
|
||||
// GET /api/providers/client - List all connections for client (includes sensitive fields for sync)
|
||||
export async function GET() {
|
||||
try {
|
||||
const connections = await getProviderConnections();
|
||||
|
||||
// Include sensitive fields for sync to cloud (only accessible from same origin)
|
||||
const clientConnections = connections.map(c => ({
|
||||
...c,
|
||||
// Don't hide sensitive fields here since this is for internal sync
|
||||
}));
|
||||
|
||||
return NextResponse.json({ connections: clientConnections });
|
||||
} catch (error) {
|
||||
console.log("Error fetching providers for client:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch providers" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
84
src/app/api/providers/route.js
Normal file
84
src/app/api/providers/route.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnections, createProviderConnection, isCloudEnabled } from "@/models";
|
||||
import { APIKEY_PROVIDERS } from "@/shared/constants/config";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
// GET /api/providers - List all connections
|
||||
export async function GET() {
|
||||
try {
|
||||
const connections = await getProviderConnections();
|
||||
|
||||
// Hide sensitive fields
|
||||
const safeConnections = connections.map(c => ({
|
||||
...c,
|
||||
apiKey: undefined,
|
||||
accessToken: undefined,
|
||||
refreshToken: undefined,
|
||||
idToken: undefined,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ connections: safeConnections });
|
||||
} catch (error) {
|
||||
console.log("Error fetching providers:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch providers" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/providers - Create new connection (API Key only, OAuth via separate flow)
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { provider, apiKey, name, priority, globalPriority, defaultModel, testStatus } = body;
|
||||
|
||||
// Validation
|
||||
if (!provider || !APIKEY_PROVIDERS[provider]) {
|
||||
return NextResponse.json({ error: "Invalid provider" }, { status: 400 });
|
||||
}
|
||||
if (!apiKey) {
|
||||
return NextResponse.json({ error: "API Key is required" }, { status: 400 });
|
||||
}
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: "Name is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const newConnection = await createProviderConnection({
|
||||
provider,
|
||||
authType: "apikey",
|
||||
name,
|
||||
apiKey,
|
||||
priority: priority || 1,
|
||||
globalPriority: globalPriority || null,
|
||||
defaultModel: defaultModel || null,
|
||||
isActive: true,
|
||||
testStatus: testStatus || "unknown",
|
||||
});
|
||||
|
||||
// Hide sensitive fields
|
||||
const result = { ...newConnection };
|
||||
delete result.apiKey;
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ connection: result }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.log("Error creating provider:", error);
|
||||
return NextResponse.json({ error: "Failed to create provider" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing providers to cloud:", error);
|
||||
}
|
||||
}
|
||||
96
src/app/api/providers/validate/route.js
Normal file
96
src/app/api/providers/validate/route.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// POST /api/providers/validate - Validate API key with provider
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { provider, apiKey } = body;
|
||||
|
||||
if (!provider || !apiKey) {
|
||||
return NextResponse.json({ error: "Provider and API key required" }, { status: 400 });
|
||||
}
|
||||
|
||||
let isValid = false;
|
||||
let error = null;
|
||||
|
||||
// Validate with each provider
|
||||
try {
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
const openaiRes = await fetch("https://api.openai.com/v1/models", {
|
||||
headers: { "Authorization": `Bearer ${apiKey}` },
|
||||
});
|
||||
isValid = openaiRes.ok;
|
||||
break;
|
||||
|
||||
case "anthropic":
|
||||
const anthropicRes = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "claude-3-haiku-20240307",
|
||||
max_tokens: 1,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
}),
|
||||
});
|
||||
isValid = anthropicRes.status !== 401;
|
||||
break;
|
||||
|
||||
case "gemini":
|
||||
const geminiRes = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`);
|
||||
isValid = geminiRes.ok;
|
||||
break;
|
||||
|
||||
case "openrouter":
|
||||
const openrouterRes = await fetch("https://openrouter.ai/api/v1/models", {
|
||||
headers: { "Authorization": `Bearer ${apiKey}` },
|
||||
});
|
||||
isValid = openrouterRes.ok;
|
||||
break;
|
||||
|
||||
case "glm":
|
||||
case "kimi":
|
||||
case "minimax": {
|
||||
const claudeBaseUrls = {
|
||||
glm: "https://api.z.ai/api/anthropic/v1/messages",
|
||||
kimi: "https://api.kimi.com/coding/v1/messages",
|
||||
minimax: "https://api.minimax.io/anthropic/v1/messages",
|
||||
};
|
||||
const claudeRes = await fetch(claudeBaseUrls[provider], {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "claude-sonnet-4-20250514",
|
||||
max_tokens: 1,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
}),
|
||||
});
|
||||
isValid = claudeRes.status !== 401;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: "Provider validation not supported" }, { status: 400 });
|
||||
}
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
valid: isValid,
|
||||
error: isValid ? null : (error || "Invalid API key"),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error validating API key:", error);
|
||||
return NextResponse.json({ error: "Validation failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
12
src/app/api/settings/route.js
Normal file
12
src/app/api/settings/route.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSettings } from "@/lib/localDb";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
return NextResponse.json(settings);
|
||||
} catch (error) {
|
||||
console.log("Error getting settings:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
12
src/app/api/shutdown/route.js
Normal file
12
src/app/api/shutdown/route.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function POST() {
|
||||
const response = NextResponse.json({ success: true, message: "Shutting down..." });
|
||||
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 500);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
255
src/app/api/sync/cloud/route.js
Normal file
255
src/app/api/sync/cloud/route.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnections, getModelAliases, getCombos, getApiKeys, createApiKey, updateProviderConnection, updateSettings } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
/**
|
||||
* POST /api/sync/cloud
|
||||
* Sync data with Cloud
|
||||
*/
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { action } = body;
|
||||
|
||||
// Always get machineId from server, don't trust client
|
||||
const machineId = await getConsistentMachineId();
|
||||
|
||||
switch (action) {
|
||||
case "enable":
|
||||
await updateSettings({ cloudEnabled: true });
|
||||
// Auto create key if none exists
|
||||
const keys = await getApiKeys();
|
||||
let createdKey = null;
|
||||
if (keys.length === 0) {
|
||||
createdKey = await createApiKey("Default Key", machineId);
|
||||
}
|
||||
return syncAndVerify(machineId, createdKey?.key, keys);
|
||||
case "sync": {
|
||||
const syncResult = await syncToCloud(machineId);
|
||||
if (syncResult.error) {
|
||||
return NextResponse.json(syncResult, { status: 502 });
|
||||
}
|
||||
return NextResponse.json(syncResult);
|
||||
}
|
||||
case "disable":
|
||||
await updateSettings({ cloudEnabled: false });
|
||||
return handleDisable(machineId, request);
|
||||
default:
|
||||
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Cloud sync error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync data to Cloud (exported for reuse)
|
||||
* @param {string} machineId
|
||||
* @param {string|null} createdKey - Key created during enable
|
||||
*/
|
||||
export async function syncToCloud(machineId, createdKey = null) {
|
||||
// Get current data from db
|
||||
const providers = await getProviderConnections();
|
||||
const modelAliases = await getModelAliases();
|
||||
const combos = await getCombos();
|
||||
const apiKeys = await getApiKeys();
|
||||
|
||||
// Send to Cloud
|
||||
const response = await fetch(`${CLOUD_URL}/sync/${machineId}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
providers,
|
||||
modelAliases,
|
||||
combos,
|
||||
apiKeys
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.log("Cloud sync failed:", errorText);
|
||||
return NextResponse.json({ error: "Cloud sync failed" }, { status: 502 });
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Update local db with tokens from Cloud (providers stored by ID)
|
||||
if (result.data && result.data.providers) {
|
||||
await updateLocalTokens(result.data.providers);
|
||||
}
|
||||
|
||||
const responseData = {
|
||||
success: true,
|
||||
message: "Synced successfully",
|
||||
changes: result.changes
|
||||
};
|
||||
|
||||
if (createdKey) {
|
||||
responseData.createdKey = createdKey;
|
||||
}
|
||||
|
||||
return responseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync and verify connection with ping
|
||||
*/
|
||||
async function syncAndVerify(machineId, createdKey, existingKeys) {
|
||||
// Step 1: Sync data to cloud
|
||||
const syncResult = await syncToCloud(machineId, createdKey);
|
||||
if (syncResult.error) {
|
||||
return NextResponse.json(syncResult, { status: 502 });
|
||||
}
|
||||
|
||||
// Step 2: Verify connection by pinging the cloud
|
||||
const apiKey = createdKey || existingKeys[0]?.key;
|
||||
if (!apiKey) {
|
||||
return NextResponse.json({
|
||||
...syncResult,
|
||||
verified: false,
|
||||
verifyError: "No API key available"
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const pingResponse = await fetch(`${CLOUD_URL}/${machineId}/v1/verify`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (pingResponse.ok) {
|
||||
return NextResponse.json({
|
||||
...syncResult,
|
||||
verified: true
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
...syncResult,
|
||||
verified: false,
|
||||
verifyError: `Ping failed: ${pingResponse.status}`
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json({
|
||||
...syncResult,
|
||||
verified: false,
|
||||
verifyError: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable Cloud - delete cache and update Claude CLI settings
|
||||
*/
|
||||
async function handleDisable(machineId, request) {
|
||||
const response = await fetch(`${CLOUD_URL}/sync/${machineId}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.log("Cloud disable failed:", errorText);
|
||||
return NextResponse.json({ error: "Failed to disable cloud" }, { status: 502 });
|
||||
}
|
||||
|
||||
// Update Claude CLI settings to use local endpoint
|
||||
const host = request.headers.get("host") || "localhost:3000";
|
||||
await updateClaudeSettingsToLocal(machineId, host);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Cloud disabled"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Claude CLI settings to use local endpoint (only if currently using cloud)
|
||||
*/
|
||||
async function updateClaudeSettingsToLocal(machineId, host) {
|
||||
try {
|
||||
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
||||
const cloudUrl = `${CLOUD_URL}/${machineId}`;
|
||||
const localUrl = `http://${host}`;
|
||||
|
||||
// Read current settings
|
||||
let settings;
|
||||
try {
|
||||
const content = await fs.readFile(settingsPath, "utf-8");
|
||||
settings = JSON.parse(content);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
return; // No settings file, nothing to update
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Check if ANTHROPIC_BASE_URL matches cloud URL
|
||||
const currentUrl = settings.env?.ANTHROPIC_BASE_URL;
|
||||
if (!currentUrl || currentUrl !== cloudUrl) {
|
||||
return; // Not using cloud URL, don't modify
|
||||
}
|
||||
|
||||
// Update to local URL
|
||||
settings.env.ANTHROPIC_BASE_URL = localUrl;
|
||||
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
||||
console.log(`Updated Claude CLI settings: ${cloudUrl} → ${localUrl}`);
|
||||
} catch (error) {
|
||||
console.log("Failed to update Claude CLI settings:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update local db with data from Cloud
|
||||
* Simple logic: if Cloud is newer, sync entire provider
|
||||
* cloudProviders is object keyed by provider ID
|
||||
*/
|
||||
async function updateLocalTokens(cloudProviders) {
|
||||
const localProviders = await getProviderConnections();
|
||||
|
||||
for (const localProvider of localProviders) {
|
||||
const cloudProvider = cloudProviders[localProvider.id];
|
||||
if (!cloudProvider) continue;
|
||||
|
||||
const cloudUpdatedAt = new Date(cloudProvider.updatedAt || 0).getTime();
|
||||
const localUpdatedAt = new Date(localProvider.updatedAt || 0).getTime();
|
||||
|
||||
// Simple logic: if Cloud is newer, sync entire provider
|
||||
if (cloudUpdatedAt > localUpdatedAt) {
|
||||
const updates = {
|
||||
// Tokens
|
||||
accessToken: cloudProvider.accessToken,
|
||||
refreshToken: cloudProvider.refreshToken,
|
||||
expiresAt: cloudProvider.expiresAt,
|
||||
expiresIn: cloudProvider.expiresIn,
|
||||
|
||||
// Provider specific data
|
||||
providerSpecificData: cloudProvider.providerSpecificData || localProvider.providerSpecificData,
|
||||
|
||||
// Status fields
|
||||
testStatus: cloudProvider.status || "active",
|
||||
lastError: cloudProvider.lastError,
|
||||
lastErrorAt: cloudProvider.lastErrorAt,
|
||||
errorCode: cloudProvider.errorCode,
|
||||
rateLimitedUntil: cloudProvider.rateLimitedUntil,
|
||||
|
||||
// Metadata
|
||||
updatedAt: cloudProvider.updatedAt
|
||||
};
|
||||
|
||||
await updateProviderConnection(localProvider.id, updates);
|
||||
console.log(`Updated ${localProvider.provider} (${localProvider.id}) from Cloud (newer: ${new Date(cloudUpdatedAt).toISOString()})`);
|
||||
} else {
|
||||
console.log(`Skipped ${localProvider.provider} (${localProvider.id}) - Local is newer or equal`);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/app/api/sync/initialize/route.js
Normal file
36
src/app/api/sync/initialize/route.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import initializeCloudSync from "@/shared/services/initializeCloudSync";
|
||||
|
||||
let syncInitialized = false;
|
||||
|
||||
// POST /api/sync/initialize - Initialize cloud sync scheduler
|
||||
export async function POST(request) {
|
||||
try {
|
||||
if (syncInitialized) {
|
||||
return NextResponse.json({
|
||||
message: "Cloud sync already initialized"
|
||||
});
|
||||
}
|
||||
|
||||
await initializeCloudSync();
|
||||
syncInitialized = true;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Cloud sync initialized successfully"
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error initializing cloud sync:", error);
|
||||
return NextResponse.json({
|
||||
error: "Failed to initialize cloud sync"
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/sync/status - Check sync initialization status
|
||||
export async function GET(request) {
|
||||
return NextResponse.json({
|
||||
initialized: syncInitialized,
|
||||
message: syncInitialized ? "Cloud sync is running" : "Cloud sync not initialized"
|
||||
});
|
||||
}
|
||||
18
src/app/api/tags/route.js
Normal file
18
src/app/api/tags/route.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ollamaModels } from "open-sse/config/ollamaModels.js";
|
||||
|
||||
const CORS_HEADERS = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
};
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, { headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return new Response(JSON.stringify(ollamaModels), {
|
||||
headers: { "Content-Type": "application/json", ...CORS_HEADERS }
|
||||
});
|
||||
}
|
||||
|
||||
30
src/app/api/usage/[connectionId]/route.js
Normal file
30
src/app/api/usage/[connectionId]/route.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { getProviderConnectionById } from "@/lib/localDb";
|
||||
import { getUsageForProvider } from "open-sse/services/usage.js";
|
||||
|
||||
/**
|
||||
* GET /api/usage/[connectionId] - Get usage data for a specific connection
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { connectionId } = await params;
|
||||
|
||||
// Get connection from database
|
||||
const connection = await getProviderConnectionById(connectionId);
|
||||
if (!connection) {
|
||||
return Response.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Only OAuth connections have usage APIs
|
||||
if (connection.authType !== "oauth") {
|
||||
return Response.json({ message: "Usage not available for API key connections" });
|
||||
}
|
||||
|
||||
// Fetch usage from provider API
|
||||
const usage = await getUsageForProvider(connection);
|
||||
return Response.json(usage);
|
||||
} catch (error) {
|
||||
console.log("Error fetching usage:", error);
|
||||
return Response.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
38
src/app/api/v1/api/chat/route.js
Normal file
38
src/app/api/v1/api/chat/route.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { handleChat } from "@/sse/handlers/chat.js";
|
||||
import { initTranslators } from "open-sse/translator/index.js";
|
||||
import { transformToOllama } from "open-sse/utils/ollamaTransform.js";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
async function ensureInitialized() {
|
||||
if (!initialized) {
|
||||
await initTranslators();
|
||||
initialized = true;
|
||||
console.log("[SSE] Translators initialized");
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request) {
|
||||
await ensureInitialized();
|
||||
|
||||
const clonedReq = request.clone();
|
||||
let modelName = "llama3.2";
|
||||
try {
|
||||
const body = await clonedReq.json();
|
||||
modelName = body.model || "llama3.2";
|
||||
} catch {}
|
||||
|
||||
const response = await handleChat(request);
|
||||
return transformToOllama(response, modelName);
|
||||
}
|
||||
|
||||
37
src/app/api/v1/chat/completions/route.js
Normal file
37
src/app/api/v1/chat/completions/route.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { callCloudWithMachineId } from "@/shared/utils/cloud.js";
|
||||
import { handleChat } from "@/sse/handlers/chat.js";
|
||||
import { initTranslators } from "open-sse/translator/index.js";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize translators once
|
||||
*/
|
||||
async function ensureInitialized() {
|
||||
if (!initialized) {
|
||||
await initTranslators();
|
||||
initialized = true;
|
||||
console.log("[SSE] Translators initialized");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CORS preflight
|
||||
*/
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request) {
|
||||
// Fallback to local handling
|
||||
await ensureInitialized();
|
||||
|
||||
return await handleChat(request);
|
||||
}
|
||||
|
||||
52
src/app/api/v1/messages/count_tokens/route.js
Normal file
52
src/app/api/v1/messages/count_tokens/route.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const CORS_HEADERS = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle CORS preflight
|
||||
*/
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, { headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/messages/count_tokens - Mock token count response
|
||||
*/
|
||||
export async function POST(request) {
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: "Invalid JSON body" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json", ...CORS_HEADERS }
|
||||
});
|
||||
}
|
||||
|
||||
// Estimate token count based on content length
|
||||
const messages = body.messages || [];
|
||||
let totalChars = 0;
|
||||
for (const msg of messages) {
|
||||
if (typeof msg.content === "string") {
|
||||
totalChars += msg.content.length;
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "text" && part.text) {
|
||||
totalChars += part.text.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rough estimate: ~4 chars per token
|
||||
const inputTokens = Math.ceil(totalChars / 4);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
input_tokens: inputTokens
|
||||
}), {
|
||||
headers: { "Content-Type": "application/json", ...CORS_HEADERS }
|
||||
});
|
||||
}
|
||||
|
||||
37
src/app/api/v1/messages/route.js
Normal file
37
src/app/api/v1/messages/route.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { handleChat } from "@/sse/handlers/chat.js";
|
||||
import { initTranslators } from "open-sse/translator/index.js";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize translators once
|
||||
*/
|
||||
async function ensureInitialized() {
|
||||
if (!initialized) {
|
||||
await initTranslators();
|
||||
initialized = true;
|
||||
console.log("[SSE] Translators initialized for /v1/messages");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CORS preflight
|
||||
*/
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/messages - Claude format (auto convert via handleChat)
|
||||
*/
|
||||
export async function POST(request) {
|
||||
await ensureInitialized();
|
||||
return await handleChat(request);
|
||||
}
|
||||
|
||||
31
src/app/api/v1/responses/route.js
Normal file
31
src/app/api/v1/responses/route.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { handleChat } from "@/sse/handlers/chat.js";
|
||||
import { initTranslators } from "open-sse/translator/index.js";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
async function ensureInitialized() {
|
||||
if (!initialized) {
|
||||
await initTranslators();
|
||||
initialized = true;
|
||||
console.log("[SSE] Translators initialized for /v1/responses");
|
||||
}
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/responses - OpenAI Responses API format
|
||||
* Now handled by translator pattern (openai-responses format auto-detected)
|
||||
*/
|
||||
export async function POST(request) {
|
||||
await ensureInitialized();
|
||||
return await handleChat(request);
|
||||
}
|
||||
32
src/app/api/v1/route.js
Normal file
32
src/app/api/v1/route.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const CORS_HEADERS = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle CORS preflight
|
||||
*/
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, { headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1 - Return models list (OpenAI compatible)
|
||||
*/
|
||||
export async function GET() {
|
||||
const models = [
|
||||
{ id: "claude-sonnet-4-20250514", object: "model", owned_by: "anthropic" },
|
||||
{ id: "claude-3-5-sonnet-20241022", object: "model", owned_by: "anthropic" },
|
||||
{ id: "gpt-4o", object: "model", owned_by: "openai" },
|
||||
{ id: "gemini-2.5-pro", object: "model", owned_by: "google" }
|
||||
];
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
object: "list",
|
||||
data: models
|
||||
}), {
|
||||
headers: { "Content-Type": "application/json", ...CORS_HEADERS }
|
||||
});
|
||||
}
|
||||
|
||||
113
src/app/api/v1beta/models/[...path]/route.js
Normal file
113
src/app/api/v1beta/models/[...path]/route.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import { handleChat } from "@/sse/handlers/chat.js";
|
||||
import { initTranslators } from "open-sse/translator/index.js";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize translators once
|
||||
*/
|
||||
async function ensureInitialized() {
|
||||
if (!initialized) {
|
||||
await initTranslators();
|
||||
initialized = true;
|
||||
console.log("[SSE] Translators initialized for /v1beta/models");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CORS preflight
|
||||
*/
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1beta/models/{model}:generateContent - Gemini compatible endpoint
|
||||
* Converts Gemini format to internal format and handles via handleChat
|
||||
*/
|
||||
export async function POST(request, { params }) {
|
||||
await ensureInitialized();
|
||||
|
||||
try {
|
||||
const { path } = await params;
|
||||
// path = ["provider", "model:generateContent"] or ["model:generateContent"]
|
||||
|
||||
let model;
|
||||
if (path.length >= 2) {
|
||||
// Format: /v1beta/models/provider/model:generateContent
|
||||
const provider = path[0];
|
||||
const modelAction = path[1];
|
||||
const modelName = modelAction.replace(":generateContent", "").replace(":streamGenerateContent", "");
|
||||
model = `${provider}/${modelName}`;
|
||||
} else {
|
||||
// Format: /v1beta/models/model:generateContent
|
||||
const modelAction = path[0];
|
||||
model = modelAction.replace(":generateContent", "").replace(":streamGenerateContent", "");
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Convert Gemini format to OpenAI/internal format
|
||||
const convertedBody = convertGeminiToInternal(body, model);
|
||||
|
||||
// Create new request with converted body
|
||||
const newRequest = new Request(request.url, {
|
||||
method: "POST",
|
||||
headers: request.headers,
|
||||
body: JSON.stringify(convertedBody),
|
||||
});
|
||||
|
||||
return await handleChat(newRequest);
|
||||
} catch (error) {
|
||||
console.log("Error handling Gemini request:", error);
|
||||
return Response.json(
|
||||
{ error: { message: error.message, code: 500 } },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Gemini request format to internal format
|
||||
*/
|
||||
function convertGeminiToInternal(geminiBody, model) {
|
||||
const messages = [];
|
||||
|
||||
// Convert system instruction
|
||||
if (geminiBody.systemInstruction) {
|
||||
const systemText = geminiBody.systemInstruction.parts
|
||||
?.map(p => p.text)
|
||||
.join("\n") || "";
|
||||
if (systemText) {
|
||||
messages.push({ role: "system", content: systemText });
|
||||
}
|
||||
}
|
||||
|
||||
// Convert contents to messages
|
||||
if (geminiBody.contents) {
|
||||
for (const content of geminiBody.contents) {
|
||||
const role = content.role === "model" ? "assistant" : "user";
|
||||
const text = content.parts?.map(p => p.text).join("\n") || "";
|
||||
messages.push({ role, content: text });
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if streaming
|
||||
const stream = geminiBody.generationConfig?.stream !== false;
|
||||
|
||||
return {
|
||||
model,
|
||||
messages,
|
||||
stream,
|
||||
max_tokens: geminiBody.generationConfig?.maxOutputTokens,
|
||||
temperature: geminiBody.generationConfig?.temperature,
|
||||
top_p: geminiBody.generationConfig?.topP,
|
||||
};
|
||||
}
|
||||
|
||||
44
src/app/api/v1beta/models/route.js
Normal file
44
src/app/api/v1beta/models/route.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { PROVIDER_MODELS } from "@/shared/constants/models";
|
||||
|
||||
/**
|
||||
* Handle CORS preflight
|
||||
*/
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1beta/models - Gemini compatible models list
|
||||
* Returns models in Gemini API format
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
// Collect all models from all providers
|
||||
const models = [];
|
||||
|
||||
for (const [provider, providerModels] of Object.entries(PROVIDER_MODELS)) {
|
||||
for (const model of providerModels) {
|
||||
models.push({
|
||||
name: `models/${provider}/${model.id}`,
|
||||
displayName: model.name || model.id,
|
||||
description: `${provider} model: ${model.name || model.id}`,
|
||||
supportedGenerationMethods: ["generateContent"],
|
||||
inputTokenLimit: 128000,
|
||||
outputTokenLimit: 8192,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ models });
|
||||
} catch (error) {
|
||||
console.log("Error fetching models:", error);
|
||||
return Response.json({ error: { message: error.message } }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user