chore: add buildOutput RTK filter, drop legacy cloud sync, internal cleanup
- feat(rtk): buildOutput filter + autodetect for npm/yarn/cargo logs - chore: remove unused cloud sync module and related routes - ui: hide deprecated providers (qwen, iflow, antigravity) - chore: minor tray/cli/internal adjustments
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { validateApiKey, getModelAliases, setModelAlias } from "@/models";
|
||||
|
||||
// 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);
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
// Auto-initialize cloud sync when server starts
|
||||
import "@/lib/initCloudSync";
|
||||
|
||||
// This API route is called automatically to initialize sync
|
||||
// This API route is called automatically to initialize app
|
||||
export async function GET() {
|
||||
return new Response("Initialized", { status: 200 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeys } from "@/lib/localDb";
|
||||
import { UPDATER_CONFIG } from "@/shared/constants/config";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
|
||||
const CLI_TOKEN_SALT = "9r-cli-auth";
|
||||
|
||||
// POST /api/models/test - Ping a single model via internal completions or embeddings
|
||||
export async function POST(request) {
|
||||
@@ -19,6 +22,8 @@ export async function POST(request) {
|
||||
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
||||
// Bypass dashboardGuard for internal self-call via CLI token (machineId-based)
|
||||
headers["x-9r-cli-token"] = await getConsistentMachineId(CLI_TOKEN_SALT);
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import { getProviderConnectionById, getApiKeys } from "@/lib/localDb";
|
||||
import { getProviderModels, PROVIDER_ID_TO_ALIAS } from "open-sse/config/providerModels.js";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
||||
import { UPDATER_CONFIG } from "@/shared/constants/config";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
|
||||
const CLI_TOKEN_SALT = "9r-cli-auth";
|
||||
|
||||
/**
|
||||
* Get an active API key to pass through auth when requireApiKey is enabled.
|
||||
@@ -16,11 +19,12 @@ async function getInternalApiKey() {
|
||||
* Ping a single model via internal completions endpoint (OpenAI format).
|
||||
* open-sse handles all provider translation automatically.
|
||||
*/
|
||||
async function pingModel(modelId, baseUrl, apiKey) {
|
||||
async function pingModel(modelId, baseUrl, apiKey, cliToken) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
||||
if (cliToken) headers["x-9r-cli-token"] = cliToken;
|
||||
const res = await fetch(`${baseUrl}/api/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
@@ -83,17 +87,19 @@ export async function POST(request, { params }) {
|
||||
}
|
||||
|
||||
const apiKey = await getInternalApiKey();
|
||||
// Bypass dashboardGuard for internal self-call via CLI token (machineId-based)
|
||||
const cliToken = await getConsistentMachineId(CLI_TOKEN_SALT);
|
||||
|
||||
// Warm up with first model to trigger token refresh (if needed) before parallel calls.
|
||||
// This prevents race condition where multiple requests concurrently refresh the same token.
|
||||
const [first, ...rest] = models;
|
||||
const firstResult = await pingModel(`${alias}/${first.id}`, baseUrl, apiKey);
|
||||
const firstResult = await pingModel(`${alias}/${first.id}`, baseUrl, apiKey, cliToken);
|
||||
const results = [{ modelId: first.id, name: first.name || first.id, ...firstResult }];
|
||||
|
||||
if (rest.length > 0) {
|
||||
const restResults = await Promise.all(
|
||||
rest.map(async (model) => {
|
||||
const result = await pingModel(`${alias}/${model.id}`, baseUrl, apiKey);
|
||||
const result = await pingModel(`${alias}/${model.id}`, baseUrl, apiKey, cliToken);
|
||||
return { modelId: model.id, name: model.name || model.id, ...result };
|
||||
})
|
||||
);
|
||||
|
||||
@@ -2,19 +2,51 @@ import { NextResponse } from "next/server";
|
||||
import { getProviderConnections } from "@/lib/localDb";
|
||||
import { backfillCodexEmails } from "@/lib/oauth/providers";
|
||||
|
||||
// GET /api/providers/client - List all connections for client (includes sensitive fields for sync)
|
||||
// Whitelist: only safe metadata fields exposed to UI
|
||||
const SAFE_FIELDS = [
|
||||
"id", "provider", "authType", "name", "email", "displayName",
|
||||
"priority", "globalPriority", "isActive", "defaultModel",
|
||||
"testStatus", "lastError", "lastErrorAt", "errorCode",
|
||||
"expiresAt", "lastUsedAt", "consecutiveUseCount",
|
||||
"createdAt", "updatedAt",
|
||||
];
|
||||
|
||||
// providerSpecificData fields safe to expose (non-secret config only)
|
||||
const SAFE_PSD_FIELDS = [
|
||||
"baseUrl", "azureEndpoint", "deployment", "apiVersion", "accountId",
|
||||
"region", "projectId", "resourceUrl", "proxyPoolId",
|
||||
"connectionProxyEnabled", "connectionProxyUrl", "connectionNoProxy",
|
||||
"githubLogin", "githubName", "githubEmail", "githubUserId",
|
||||
"username", "firstName", "lastName", "authMethod", "authKind",
|
||||
];
|
||||
|
||||
function maskName(name) {
|
||||
if (typeof name !== "string" || name.length <= 16) return name;
|
||||
// Names like "hahask-uDUOg90..." may embed API keys — mask if looks like key
|
||||
if (/[a-zA-Z0-9_-]{32,}/.test(name)) return `${name.slice(0, 8)}***`;
|
||||
return name;
|
||||
}
|
||||
|
||||
function sanitize(c) {
|
||||
const safe = {};
|
||||
for (const f of SAFE_FIELDS) if (c[f] !== undefined) safe[f] = c[f];
|
||||
if (safe.name) safe.name = maskName(safe.name);
|
||||
if (c.providerSpecificData) {
|
||||
const psd = {};
|
||||
for (const f of SAFE_PSD_FIELDS) {
|
||||
if (c.providerSpecificData[f] !== undefined) psd[f] = c.providerSpecificData[f];
|
||||
}
|
||||
safe.providerSpecificData = psd;
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
// GET /api/providers/client - List connections for dashboard UI (whitelist only)
|
||||
export async function GET() {
|
||||
try {
|
||||
await backfillCodexEmails();
|
||||
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 });
|
||||
return NextResponse.json({ connections: connections.map(sanitize) });
|
||||
} catch (error) {
|
||||
console.log("Error fetching providers for client:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch providers" }, { status: 500 });
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { callCloudWithMachineId } from "@/shared/utils/cloud.js";
|
||||
import { handleChat } from "@/sse/handlers/chat.js";
|
||||
import { initTranslators } from "open-sse/translator/index.js";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user