mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
chore: apply PR 1495 and update changelog
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
"use server";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import { getMitmAlias, setMitmAliasAll } from "@/models";
|
||||
import { cliMitmAliasUpdateSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
|
||||
// GET - Get MITM aliases for a tool
|
||||
export async function GET(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const toolName = searchParams.get("tool");
|
||||
@@ -20,6 +24,9 @@ export async function GET(request) {
|
||||
|
||||
// PUT - Save MITM aliases for a specific tool
|
||||
export async function PUT(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
export const runtime = "nodejs";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import { cliMitmStartSchema, cliMitmStopSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { resolveApiKey } from "@/shared/services/apiKeyResolver";
|
||||
|
||||
// GET - Check MITM status
|
||||
export async function GET() {
|
||||
export async function GET(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { getMitmStatus, getCachedPassword } = await import("@/mitm/manager");
|
||||
const status = await getMitmStatus();
|
||||
@@ -27,6 +31,9 @@ export async function GET() {
|
||||
|
||||
// POST - Start MITM proxy
|
||||
export async function POST(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -81,6 +88,9 @@ export async function POST(request) {
|
||||
|
||||
// DELETE - Stop MITM proxy
|
||||
export async function DELETE(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import { listBackups, restoreBackup, deleteBackup } from "@/shared/services/backupService";
|
||||
import { ensureCliConfigWriteAllowed } from "@/shared/services/cliRuntime";
|
||||
import { cliBackupMutationSchema } from "@/shared/validation/schemas";
|
||||
@@ -10,6 +11,9 @@ const VALID_TOOLS = ["claude", "codex", "droid", "openclaw", "cline", "kilo", "q
|
||||
|
||||
// GET /api/cli-tools/backups?tool=claude — list backups
|
||||
export async function GET(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const tool = searchParams.get("tool") || searchParams.get("toolId");
|
||||
@@ -37,6 +41,9 @@ export async function GET(request) {
|
||||
|
||||
// POST /api/cli-tools/backups { tool, backupId } — restore a backup
|
||||
export async function POST(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -86,6 +93,9 @@ export async function POST(request) {
|
||||
|
||||
// DELETE /api/cli-tools/backups { tool, backupId } — delete a backup
|
||||
export async function DELETE(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import {
|
||||
ensureCliConfigWriteAllowed,
|
||||
getCliPrimaryConfigPath,
|
||||
@@ -32,7 +33,10 @@ const readSettings = async () => {
|
||||
};
|
||||
|
||||
// GET - Check claude CLI and read current settings
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const runtime = await getCliRuntimeStatus("claude");
|
||||
|
||||
@@ -74,6 +78,9 @@ export async function GET() {
|
||||
|
||||
// POST - Backup old fields and write new settings
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -186,7 +193,10 @@ const RESET_ENV_KEYS = [
|
||||
];
|
||||
|
||||
// DELETE - Reset settings (remove env fields)
|
||||
export async function DELETE() {
|
||||
export async function DELETE(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const writeGuard = ensureCliConfigWriteAllowed();
|
||||
if (writeGuard) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NextResponse } from "next/server";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import { ensureCliConfigWriteAllowed, getCliRuntimeStatus } from "@/shared/services/cliRuntime";
|
||||
import { createBackup } from "@/shared/services/backupService";
|
||||
import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState";
|
||||
@@ -52,7 +53,10 @@ const hasOmniRouteConfig = (globalState: any) => {
|
||||
};
|
||||
|
||||
// GET - Check cline CLI and read current settings
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const runtime = await getCliRuntimeStatus("cline");
|
||||
|
||||
@@ -101,6 +105,9 @@ export async function GET() {
|
||||
|
||||
// POST - Configure Cline to use OmniRoute as OpenAI-compatible provider
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -193,7 +200,10 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// DELETE - Remove OmniRoute OpenAI-compatible provider config
|
||||
export async function DELETE() {
|
||||
export async function DELETE(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const writeGuard = ensureCliConfigWriteAllowed();
|
||||
if (writeGuard) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import { ensureCliConfigWriteAllowed, getCliConfigPaths } from "@/shared/services/cliRuntime";
|
||||
import { resolveDataDir } from "@/lib/dataPaths";
|
||||
import { codexProfileIdSchema, codexProfileNameSchema } from "@/shared/validation/schemas";
|
||||
@@ -52,7 +53,10 @@ function extractAuthLabel(authJson) {
|
||||
}
|
||||
|
||||
// GET - List all saved profiles
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
await ensureProfilesDir();
|
||||
|
||||
@@ -94,6 +98,9 @@ export async function GET() {
|
||||
|
||||
// POST - Save current config as a named profile
|
||||
export async function POST(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -180,6 +187,9 @@ export async function POST(request) {
|
||||
|
||||
// PUT - Activate a saved profile (restore its config + auth)
|
||||
export async function PUT(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -251,6 +261,9 @@ export async function PUT(request) {
|
||||
|
||||
// DELETE - Remove a saved profile
|
||||
export async function DELETE(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import {
|
||||
ensureCliConfigWriteAllowed,
|
||||
getCliConfigPaths,
|
||||
@@ -120,7 +121,10 @@ const hasOmniRouteConfig = (config: string | null) => {
|
||||
};
|
||||
|
||||
// GET - Check codex CLI and read current settings
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const runtime = await getCliRuntimeStatus("codex");
|
||||
|
||||
@@ -161,6 +165,9 @@ export async function GET() {
|
||||
|
||||
// POST - Update OmniRoute settings (merge with existing config)
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -301,7 +308,10 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// DELETE - Remove OmniRoute settings only (keep other settings)
|
||||
export async function DELETE() {
|
||||
export async function DELETE(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const writeGuard = ensureCliConfigWriteAllowed();
|
||||
if (writeGuard) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import {
|
||||
ensureCliConfigWriteAllowed,
|
||||
getCliPrimaryConfigPath,
|
||||
@@ -36,7 +37,10 @@ const hasOmniRouteConfig = (settings: any) => {
|
||||
};
|
||||
|
||||
// GET - Check droid CLI and read current settings
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const runtime = await getCliRuntimeStatus("droid");
|
||||
|
||||
@@ -77,6 +81,9 @@ export async function GET() {
|
||||
|
||||
// POST - Update OmniRoute customModels (merge with existing settings)
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -174,7 +181,10 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// DELETE - Remove OmniRoute customModels only (keep other settings)
|
||||
export async function DELETE() {
|
||||
export async function DELETE(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const writeGuard = ensureCliConfigWriteAllowed();
|
||||
if (writeGuard) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import { getRuntimePorts } from "@/lib/runtime/ports";
|
||||
import { getOpenCodeConfigPath } from "@/shared/services/cliRuntime";
|
||||
import { mergeOpenCodeConfig } from "@/shared/services/opencodeConfig";
|
||||
@@ -16,6 +17,9 @@ import { resolveApiKey } from "@/shared/services/apiKeyResolver";
|
||||
* Currently supports: continue, opencode
|
||||
*/
|
||||
export async function POST(request, { params }) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NextResponse } from "next/server";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import { ensureCliConfigWriteAllowed, getCliRuntimeStatus } from "@/shared/services/cliRuntime";
|
||||
import { createBackup } from "@/shared/services/backupService";
|
||||
import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState";
|
||||
@@ -38,7 +39,10 @@ const hasOmniRouteConfig = (auth) => {
|
||||
};
|
||||
|
||||
// GET - Check kilo CLI and read current settings
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const runtime = await getCliRuntimeStatus("kilo");
|
||||
|
||||
@@ -109,6 +113,9 @@ export async function GET() {
|
||||
|
||||
// POST - Configure Kilo Code to use OmniRoute as OpenAI-compatible provider
|
||||
export async function POST(request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -217,7 +224,10 @@ export async function POST(request) {
|
||||
}
|
||||
|
||||
// DELETE - Remove OmniRoute config from Kilo
|
||||
export async function DELETE() {
|
||||
export async function DELETE(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const writeGuard = ensureCliConfigWriteAllowed();
|
||||
if (writeGuard) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import {
|
||||
ensureCliConfigWriteAllowed,
|
||||
getCliPrimaryConfigPath,
|
||||
@@ -36,7 +37,10 @@ const hasOmniRouteConfig = (settings: any) => {
|
||||
};
|
||||
|
||||
// GET - Check openclaw CLI and read current settings
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const runtime = await getCliRuntimeStatus("openclaw");
|
||||
|
||||
@@ -77,6 +81,9 @@ export async function GET() {
|
||||
|
||||
// POST - Update OmniRoute settings (merge with existing settings)
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -174,7 +181,10 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// DELETE - Remove OmniRoute settings only (keep other settings)
|
||||
export async function DELETE() {
|
||||
export async function DELETE(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const writeGuard = ensureCliConfigWriteAllowed();
|
||||
if (writeGuard) {
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import { getComboModelProvider } from "@/lib/combos/steps";
|
||||
import { resolveOmniRouteBaseUrl } from "@/shared/utils/resolveOmniRouteBaseUrl";
|
||||
|
||||
const OMNIROUTE_BASE_URL = resolveOmniRouteBaseUrl();
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
// Fetch current health and combos to determine best provider ordering
|
||||
const [healthRes, combosRes] = await Promise.allSettled([
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NextResponse } from "next/server";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import {
|
||||
ensureCliConfigWriteAllowed,
|
||||
getCliPrimaryConfigPath,
|
||||
@@ -64,7 +65,10 @@ const hasOmniRouteConfig = (settings: any) => {
|
||||
};
|
||||
|
||||
// GET - Check Qwen CLI and read current settings
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const runtime = await getCliRuntimeStatus("qwen");
|
||||
|
||||
@@ -106,6 +110,9 @@ export async function GET() {
|
||||
|
||||
// POST - Write OmniRoute config to settings.json + .env
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
@@ -272,7 +279,10 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// DELETE - Remove OmniRoute config from settings.json and .env
|
||||
export async function DELETE() {
|
||||
export async function DELETE(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const writeGuard = ensureCliConfigWriteAllowed();
|
||||
if (writeGuard) {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"use server";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import {
|
||||
CLI_TOOL_IDS,
|
||||
getCliPrimaryConfigPath,
|
||||
getCliRuntimeStatus,
|
||||
} from "@/shared/services/cliRuntime";
|
||||
|
||||
export async function GET(_request, { params }) {
|
||||
export async function GET(request, { params }) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { toolId } = await params;
|
||||
const normalizedToolId = String(toolId || "")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "fs/promises";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import {
|
||||
getCliRuntimeStatus,
|
||||
CLI_TOOL_IDS,
|
||||
@@ -99,7 +100,10 @@ async function checkToolConfigStatus(toolId: string): Promise<string> {
|
||||
* Returns runtime + config status for all CLI tools in one batch call.
|
||||
* Used by the CLI Tools page to show status badges in collapsed state.
|
||||
*/
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireCliToolsAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const statuses = {};
|
||||
|
||||
|
||||
50
src/app/api/v1/_helpers/apiKeyScope.ts
Normal file
50
src/app/api/v1/_helpers/apiKeyScope.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getApiKeyMetadata } from "@/lib/localDb";
|
||||
import { CORS_HEADERS } from "@/shared/utils/cors";
|
||||
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
|
||||
|
||||
export interface ApiKeyRequestScope {
|
||||
apiKey: string | null;
|
||||
apiKeyId: string | null;
|
||||
apiKeyMetadata: Awaited<ReturnType<typeof getApiKeyMetadata>>;
|
||||
rejection: Response | null;
|
||||
}
|
||||
|
||||
export async function getApiKeyRequestScope(request: Request): Promise<ApiKeyRequestScope> {
|
||||
const apiKey = extractApiKey(request);
|
||||
|
||||
if (process.env.REQUIRE_API_KEY === "true") {
|
||||
if (!apiKey) {
|
||||
return {
|
||||
apiKey: null,
|
||||
apiKeyId: null,
|
||||
apiKeyMetadata: null,
|
||||
rejection: NextResponse.json(
|
||||
{ error: { message: "Missing API key", type: "invalid_request_error" } },
|
||||
{ status: 401, headers: CORS_HEADERS }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (!(await isValidApiKey(apiKey))) {
|
||||
return {
|
||||
apiKey: null,
|
||||
apiKeyId: null,
|
||||
apiKeyMetadata: null,
|
||||
rejection: NextResponse.json(
|
||||
{ error: { message: "Invalid API key", type: "invalid_request_error" } },
|
||||
{ status: 401, headers: CORS_HEADERS }
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const apiKeyMetadata = await getApiKeyMetadata(apiKey);
|
||||
return {
|
||||
apiKey,
|
||||
apiKeyId: apiKeyMetadata?.id || null,
|
||||
apiKeyMetadata,
|
||||
rejection: null,
|
||||
};
|
||||
}
|
||||
@@ -1,71 +1,73 @@
|
||||
import { CORS_HEADERS } from "@/shared/utils/cors";
|
||||
import { extractApiKey } from "@/sse/services/auth";
|
||||
import { getBatch, updateBatch, getApiKeyMetadata } from "@/lib/localDb";
|
||||
import { getBatch, updateBatch } from "@/lib/localDb";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeyRequestScope } from "@/app/api/v1/_helpers/apiKeyScope";
|
||||
|
||||
function formatBatchResponse(batch: any) {
|
||||
return {
|
||||
id: batch.id,
|
||||
object: "batch",
|
||||
endpoint: batch.endpoint,
|
||||
errors: batch.errors || null,
|
||||
input_file_id: batch.inputFileId,
|
||||
completion_window: batch.completionWindow,
|
||||
status: batch.status,
|
||||
output_file_id: batch.outputFileId || null,
|
||||
error_file_id: batch.errorFileId || null,
|
||||
created_at: batch.createdAt,
|
||||
in_progress_at: batch.inProgressAt || null,
|
||||
expires_at: batch.expiresAt || null,
|
||||
finalizing_at: batch.finalizingAt || null,
|
||||
completed_at: batch.completedAt || null,
|
||||
failed_at: batch.failedAt || null,
|
||||
expired_at: batch.expiredAt || null,
|
||||
cancelling_at: batch.cancellingAt || null,
|
||||
cancelled_at: batch.cancelledAt || null,
|
||||
request_counts: {
|
||||
total: batch.requestCountsTotal || 0,
|
||||
completed: batch.requestCountsCompleted || 0,
|
||||
failed: batch.requestCountsFailed || 0,
|
||||
},
|
||||
metadata: batch.metadata || null,
|
||||
model: batch.model || null,
|
||||
usage: batch.usage || null,
|
||||
};
|
||||
return {
|
||||
id: batch.id,
|
||||
object: "batch",
|
||||
endpoint: batch.endpoint,
|
||||
errors: batch.errors || null,
|
||||
input_file_id: batch.inputFileId,
|
||||
completion_window: batch.completionWindow,
|
||||
status: batch.status,
|
||||
output_file_id: batch.outputFileId || null,
|
||||
error_file_id: batch.errorFileId || null,
|
||||
created_at: batch.createdAt,
|
||||
in_progress_at: batch.inProgressAt || null,
|
||||
expires_at: batch.expiresAt || null,
|
||||
finalizing_at: batch.finalizingAt || null,
|
||||
completed_at: batch.completedAt || null,
|
||||
failed_at: batch.failedAt || null,
|
||||
expired_at: batch.expiredAt || null,
|
||||
cancelling_at: batch.cancellingAt || null,
|
||||
cancelled_at: batch.cancelledAt || null,
|
||||
request_counts: {
|
||||
total: batch.requestCountsTotal || 0,
|
||||
completed: batch.requestCountsCompleted || 0,
|
||||
failed: batch.requestCountsFailed || 0,
|
||||
},
|
||||
metadata: batch.metadata || null,
|
||||
model: batch.model || null,
|
||||
usage: batch.usage || null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const apiKey = extractApiKey(request);
|
||||
const apiKeyMetadata = await getApiKeyMetadata(apiKey);
|
||||
const apiKeyId = apiKeyMetadata?.id || null;
|
||||
const scope = await getApiKeyRequestScope(request);
|
||||
if (scope.rejection) return scope.rejection;
|
||||
const apiKeyId = scope.apiKeyId;
|
||||
|
||||
const { id } = await params;
|
||||
const batch = getBatch(id);
|
||||
const { id } = await params;
|
||||
const batch = getBatch(id);
|
||||
|
||||
if (!batch || (batch.apiKeyId !== null && batch.apiKeyId !== apiKeyId)) {
|
||||
return NextResponse.json(
|
||||
{ error: { message: "Batch not found", type: "invalid_request_error" } },
|
||||
{ status: 404, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
if (!batch || (batch.apiKeyId !== null && batch.apiKeyId !== apiKeyId)) {
|
||||
return NextResponse.json(
|
||||
{ error: { message: "Batch not found", type: "invalid_request_error" } },
|
||||
{ status: 404, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
|
||||
if (["completed", "failed", "cancelled", "expired"].includes(batch.status)) {
|
||||
return NextResponse.json(
|
||||
{ error: { message: `Batch ${id} is already ${batch.status}`, type: "invalid_request_error" } },
|
||||
{ status: 400, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
if (["completed", "failed", "cancelled", "expired"].includes(batch.status)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: { message: `Batch ${id} is already ${batch.status}`, type: "invalid_request_error" },
|
||||
},
|
||||
{ status: 400, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
|
||||
if (batch.status === "cancelling") {
|
||||
return NextResponse.json(formatBatchResponse(batch), { headers: CORS_HEADERS });
|
||||
}
|
||||
if (batch.status === "cancelling") {
|
||||
return NextResponse.json(formatBatchResponse(batch), { headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
updateBatch(id, {
|
||||
status: "cancelling",
|
||||
cancellingAt: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
updateBatch(id, {
|
||||
status: "cancelling",
|
||||
cancellingAt: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
const updatedBatch = getBatch(id);
|
||||
const updatedBatch = getBatch(id);
|
||||
|
||||
return NextResponse.json(formatBatchResponse(updatedBatch), { headers: CORS_HEADERS });
|
||||
return NextResponse.json(formatBatchResponse(updatedBatch), { headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
import { CORS_HEADERS } from "@/shared/utils/cors";
|
||||
import { extractApiKey } from "@/sse/services/auth";
|
||||
import { getBatch, getApiKeyMetadata } from "@/lib/localDb";
|
||||
import { getBatch } from "@/lib/localDb";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeyRequestScope } from "@/app/api/v1/_helpers/apiKeyScope";
|
||||
|
||||
function formatBatchResponse(batch: any) {
|
||||
return {
|
||||
id: batch.id,
|
||||
object: "batch",
|
||||
endpoint: batch.endpoint,
|
||||
errors: batch.errors || null,
|
||||
input_file_id: batch.inputFileId,
|
||||
completion_window: batch.completionWindow,
|
||||
status: batch.status,
|
||||
output_file_id: batch.outputFileId || null,
|
||||
error_file_id: batch.errorFileId || null,
|
||||
created_at: batch.createdAt,
|
||||
in_progress_at: batch.inProgressAt || null,
|
||||
expires_at: batch.expiresAt || null,
|
||||
finalizing_at: batch.finalizingAt || null,
|
||||
completed_at: batch.completedAt || null,
|
||||
failed_at: batch.failedAt || null,
|
||||
expired_at: batch.expiredAt || null,
|
||||
cancelling_at: batch.cancellingAt || null,
|
||||
cancelled_at: batch.cancelledAt || null,
|
||||
request_counts: {
|
||||
total: batch.requestCountsTotal || 0,
|
||||
completed: batch.requestCountsCompleted || 0,
|
||||
failed: batch.requestCountsFailed || 0,
|
||||
},
|
||||
metadata: batch.metadata || null,
|
||||
model: batch.model || null,
|
||||
usage: batch.usage || null,
|
||||
};
|
||||
return {
|
||||
id: batch.id,
|
||||
object: "batch",
|
||||
endpoint: batch.endpoint,
|
||||
errors: batch.errors || null,
|
||||
input_file_id: batch.inputFileId,
|
||||
completion_window: batch.completionWindow,
|
||||
status: batch.status,
|
||||
output_file_id: batch.outputFileId || null,
|
||||
error_file_id: batch.errorFileId || null,
|
||||
created_at: batch.createdAt,
|
||||
in_progress_at: batch.inProgressAt || null,
|
||||
expires_at: batch.expiresAt || null,
|
||||
finalizing_at: batch.finalizingAt || null,
|
||||
completed_at: batch.completedAt || null,
|
||||
failed_at: batch.failedAt || null,
|
||||
expired_at: batch.expiredAt || null,
|
||||
cancelling_at: batch.cancellingAt || null,
|
||||
cancelled_at: batch.cancelledAt || null,
|
||||
request_counts: {
|
||||
total: batch.requestCountsTotal || 0,
|
||||
completed: batch.requestCountsCompleted || 0,
|
||||
failed: batch.requestCountsFailed || 0,
|
||||
},
|
||||
metadata: batch.metadata || null,
|
||||
model: batch.model || null,
|
||||
usage: batch.usage || null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const apiKey = extractApiKey(request);
|
||||
const apiKeyMetadata = await getApiKeyMetadata(apiKey);
|
||||
const apiKeyId = apiKeyMetadata?.id || null;
|
||||
const scope = await getApiKeyRequestScope(request);
|
||||
if (scope.rejection) return scope.rejection;
|
||||
const apiKeyId = scope.apiKeyId;
|
||||
|
||||
const { id } = await params;
|
||||
const batch = getBatch(id);
|
||||
const { id } = await params;
|
||||
const batch = getBatch(id);
|
||||
|
||||
if (!batch || (batch.apiKeyId !== null && batch.apiKeyId !== apiKeyId)) {
|
||||
return NextResponse.json(
|
||||
{ error: { message: "Batch not found", type: "invalid_request_error" } },
|
||||
{ status: 404, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
if (!batch || (batch.apiKeyId !== null && batch.apiKeyId !== apiKeyId)) {
|
||||
return NextResponse.json(
|
||||
{ error: { message: "Batch not found", type: "invalid_request_error" } },
|
||||
{ status: 404, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(formatBatchResponse(batch), { headers: CORS_HEADERS });
|
||||
return NextResponse.json(formatBatchResponse(batch), { headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import { CORS_HEADERS } from "@/shared/utils/cors";
|
||||
import { extractApiKey } from "@/sse/services/auth";
|
||||
import { createBatch, getApiKeyMetadata, getFile, listBatches } from "@/lib/localDb";
|
||||
import { createBatch, getFile, listBatches } from "@/lib/localDb";
|
||||
import { v1BatchCreateSchema } from "@/shared/validation/schemas";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeyRequestScope } from "@/app/api/v1/_helpers/apiKeyScope";
|
||||
|
||||
function formatBatchResponse(batch: any) {
|
||||
return {
|
||||
id: batch.id,
|
||||
object: "batch",
|
||||
endpoint: batch.endpoint,
|
||||
errors: batch.errors || null,
|
||||
input_file_id: batch.inputFileId,
|
||||
completion_window: batch.completionWindow,
|
||||
status: batch.status,
|
||||
output_file_id: batch.outputFileId || null,
|
||||
error_file_id: batch.errorFileId || null,
|
||||
created_at: batch.createdAt,
|
||||
in_progress_at: batch.inProgressAt || null,
|
||||
expires_at: batch.expiresAt || null,
|
||||
finalizing_at: batch.finalizingAt || null,
|
||||
completed_at: batch.completedAt || null,
|
||||
failed_at: batch.failedAt || null,
|
||||
expired_at: batch.expiredAt || null,
|
||||
cancelling_at: batch.cancellingAt || null,
|
||||
cancelled_at: batch.cancelledAt || null,
|
||||
request_counts: {
|
||||
total: batch.requestCountsTotal || 0,
|
||||
completed: batch.requestCountsCompleted || 0,
|
||||
failed: batch.requestCountsFailed || 0,
|
||||
},
|
||||
metadata: batch.metadata || null,
|
||||
model: batch.model || null,
|
||||
usage: batch.usage || null,
|
||||
};
|
||||
return {
|
||||
id: batch.id,
|
||||
object: "batch",
|
||||
endpoint: batch.endpoint,
|
||||
errors: batch.errors || null,
|
||||
input_file_id: batch.inputFileId,
|
||||
completion_window: batch.completionWindow,
|
||||
status: batch.status,
|
||||
output_file_id: batch.outputFileId || null,
|
||||
error_file_id: batch.errorFileId || null,
|
||||
created_at: batch.createdAt,
|
||||
in_progress_at: batch.inProgressAt || null,
|
||||
expires_at: batch.expiresAt || null,
|
||||
finalizing_at: batch.finalizingAt || null,
|
||||
completed_at: batch.completedAt || null,
|
||||
failed_at: batch.failedAt || null,
|
||||
expired_at: batch.expiredAt || null,
|
||||
cancelling_at: batch.cancellingAt || null,
|
||||
cancelled_at: batch.cancelledAt || null,
|
||||
request_counts: {
|
||||
total: batch.requestCountsTotal || 0,
|
||||
completed: batch.requestCountsCompleted || 0,
|
||||
failed: batch.requestCountsFailed || 0,
|
||||
},
|
||||
metadata: batch.metadata || null,
|
||||
model: batch.model || null,
|
||||
usage: batch.usage || null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const apiKey = extractApiKey(request);
|
||||
const apiKeyMetadata = await getApiKeyMetadata(apiKey);
|
||||
const apiKeyId = apiKeyMetadata?.id || null;
|
||||
const scope = await getApiKeyRequestScope(request);
|
||||
if (scope.rejection) return scope.rejection;
|
||||
const apiKeyId = scope.apiKeyId;
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
@@ -46,55 +46,60 @@ export async function POST(request: Request) {
|
||||
|
||||
const inputFile = getFile(validated.input_file_id);
|
||||
if (!inputFile || (inputFile.apiKeyId !== null && inputFile.apiKeyId !== apiKeyId)) {
|
||||
return NextResponse.json(
|
||||
{ error: { message: "Input file not found", type: "invalid_request_error" } },
|
||||
{ status: 400, headers: CORS_HEADERS }
|
||||
);
|
||||
return NextResponse.json(
|
||||
{ error: { message: "Input file not found", type: "invalid_request_error" } },
|
||||
{ status: 400, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
|
||||
const batch = createBatch({
|
||||
endpoint: validated.endpoint as any,
|
||||
completionWindow: validated.completion_window,
|
||||
inputFileId: validated.input_file_id,
|
||||
metadata: validated.metadata,
|
||||
apiKeyId,
|
||||
outputExpiresAfterSeconds: validated.output_expires_after?.seconds || null,
|
||||
outputExpiresAfterAnchor: validated.output_expires_after?.anchor || null,
|
||||
endpoint: validated.endpoint as any,
|
||||
completionWindow: validated.completion_window,
|
||||
inputFileId: validated.input_file_id,
|
||||
metadata: validated.metadata,
|
||||
apiKeyId,
|
||||
outputExpiresAfterSeconds: validated.output_expires_after?.seconds || null,
|
||||
outputExpiresAfterAnchor: validated.output_expires_after?.anchor || null,
|
||||
});
|
||||
|
||||
return NextResponse.json(formatBatchResponse(batch), { headers: CORS_HEADERS });
|
||||
} catch (error) {
|
||||
console.error("[BATCHES] Create failed:", error);
|
||||
return NextResponse.json(
|
||||
{ error: { message: error instanceof Error ? error.message : "Create failed", type: "invalid_request_error" } },
|
||||
{
|
||||
error: {
|
||||
message: error instanceof Error ? error.message : "Create failed",
|
||||
type: "invalid_request_error",
|
||||
},
|
||||
},
|
||||
{ status: 400, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const apiKey = extractApiKey(request);
|
||||
const apiKeyMetadata = await getApiKeyMetadata(apiKey);
|
||||
const apiKeyId = apiKeyMetadata?.id || null;
|
||||
const scope = await getApiKeyRequestScope(request);
|
||||
if (scope.rejection) return scope.rejection;
|
||||
const apiKeyId = scope.apiKeyId;
|
||||
|
||||
const url = new URL(request.url);
|
||||
const limit = Number.parseInt(url.searchParams.get("limit") || "20");
|
||||
const after = url.searchParams.get("after") || undefined;
|
||||
const url = new URL(request.url);
|
||||
const limit = Number.parseInt(url.searchParams.get("limit") || "20");
|
||||
const after = url.searchParams.get("after") || undefined;
|
||||
|
||||
const batches = listBatches(apiKeyId || undefined, limit + 1, after);
|
||||
const hasMore = batches.length > limit;
|
||||
const data = hasMore ? batches.slice(0, limit) : batches;
|
||||
|
||||
const formattedData = data.map(b => formatBatchResponse(b));
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
object: "list",
|
||||
data: formattedData,
|
||||
first_id: formattedData.length > 0 ? formattedData[0].id : null,
|
||||
last_id: formattedData.length > 0 ? formattedData.at(-1).id : null,
|
||||
has_more: hasMore,
|
||||
},
|
||||
{ headers: CORS_HEADERS }
|
||||
);
|
||||
const batches = listBatches(apiKeyId || undefined, limit + 1, after);
|
||||
const hasMore = batches.length > limit;
|
||||
const data = hasMore ? batches.slice(0, limit) : batches;
|
||||
|
||||
const formattedData = data.map((b) => formatBatchResponse(b));
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
object: "list",
|
||||
data: formattedData,
|
||||
first_id: formattedData.length > 0 ? formattedData[0].id : null,
|
||||
last_id: formattedData.length > 0 ? formattedData.at(-1).id : null,
|
||||
has_more: hasMore,
|
||||
},
|
||||
{ headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CORS_HEADERS } from "@/shared/utils/cors";
|
||||
import { extractApiKey } from "@/sse/services/auth";
|
||||
import { getFile, getFileContent, getApiKeyMetadata } from "@/lib/localDb";
|
||||
import { getFile, getFileContent } from "@/lib/localDb";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeyRequestScope } from "@/app/api/v1/_helpers/apiKeyScope";
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const apiKey = extractApiKey(request);
|
||||
const apiKeyMetadata = await getApiKeyMetadata(apiKey);
|
||||
const apiKeyId = apiKeyMetadata?.id || null;
|
||||
const scope = await getApiKeyRequestScope(request);
|
||||
if (scope.rejection) return scope.rejection;
|
||||
const apiKeyId = scope.apiKeyId;
|
||||
|
||||
const { id } = await params;
|
||||
const file = getFile(id);
|
||||
@@ -20,7 +20,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id:
|
||||
|
||||
const content = getFileContent(id);
|
||||
if (!content) {
|
||||
return NextResponse.json(
|
||||
return NextResponse.json(
|
||||
{ error: { message: "File content not found", type: "invalid_request_error" } },
|
||||
{ status: 404, headers: CORS_HEADERS }
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CORS_HEADERS } from "@/shared/utils/cors";
|
||||
import { extractApiKey } from "@/sse/services/auth";
|
||||
import { getFile, deleteFile, getApiKeyMetadata, formatFileResponse } from "@/lib/localDb";
|
||||
import { getFile, deleteFile, formatFileResponse } from "@/lib/localDb";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeyRequestScope } from "@/app/api/v1/_helpers/apiKeyScope";
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const apiKey = extractApiKey(request);
|
||||
const apiKeyMetadata = await getApiKeyMetadata(apiKey);
|
||||
const apiKeyId = apiKeyMetadata?.id || null;
|
||||
const scope = await getApiKeyRequestScope(request);
|
||||
if (scope.rejection) return scope.rejection;
|
||||
const apiKeyId = scope.apiKeyId;
|
||||
|
||||
const { id } = await params;
|
||||
const file = getFile(id);
|
||||
@@ -17,14 +17,14 @@ export async function GET(request: Request, { params }: { params: Promise<{ id:
|
||||
{ status: 404, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return NextResponse.json(formatFileResponse(file), { headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const apiKey = extractApiKey(request);
|
||||
const apiKeyMetadata = await getApiKeyMetadata(apiKey);
|
||||
const apiKeyId = apiKeyMetadata?.id || null;
|
||||
const scope = await getApiKeyRequestScope(request);
|
||||
if (scope.rejection) return scope.rejection;
|
||||
const apiKeyId = scope.apiKeyId;
|
||||
|
||||
const { id } = await params;
|
||||
const file = getFile(id);
|
||||
@@ -38,9 +38,12 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ i
|
||||
|
||||
deleteFile(id);
|
||||
|
||||
return NextResponse.json({
|
||||
id,
|
||||
object: "file",
|
||||
deleted: true
|
||||
}, { headers: CORS_HEADERS });
|
||||
return NextResponse.json(
|
||||
{
|
||||
id,
|
||||
object: "file",
|
||||
deleted: true,
|
||||
},
|
||||
{ headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CORS_HEADERS } from "@/shared/utils/cors";
|
||||
import { extractApiKey } from "@/sse/services/auth";
|
||||
import { createFile, listFiles, getApiKeyMetadata, formatFileResponse } from "@/lib/localDb";
|
||||
import { createFile, listFiles, formatFileResponse } from "@/lib/localDb";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeyRequestScope } from "@/app/api/v1/_helpers/apiKeyScope";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const apiKey = extractApiKey(request);
|
||||
const apiKeyMetadata = await getApiKeyMetadata(apiKey);
|
||||
const apiKeyId = apiKeyMetadata?.id || null;
|
||||
const scope = await getApiKeyRequestScope(request);
|
||||
if (scope.rejection) return scope.rejection;
|
||||
const apiKeyId = scope.apiKeyId;
|
||||
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
@@ -25,7 +25,12 @@ export async function POST(request: Request) {
|
||||
const MAX_FILE_BYTES = 512 * 1024 * 1024; // 512 MB
|
||||
if (file.size > MAX_FILE_BYTES) {
|
||||
return NextResponse.json(
|
||||
{ error: { message: "File exceeds maximum allowed size of 512 MB", type: "invalid_request_error" } },
|
||||
{
|
||||
error: {
|
||||
message: "File exceeds maximum allowed size of 512 MB",
|
||||
type: "invalid_request_error",
|
||||
},
|
||||
},
|
||||
{ status: 400, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
@@ -61,7 +66,7 @@ export async function POST(request: Request) {
|
||||
apiKeyId,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
|
||||
return NextResponse.json(formatFileResponse(record), { headers: CORS_HEADERS });
|
||||
} catch (error) {
|
||||
console.error("[FILES] Upload failed:", error);
|
||||
@@ -73,9 +78,9 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const apiKey = extractApiKey(request);
|
||||
const apiKeyMetadata = await getApiKeyMetadata(apiKey);
|
||||
const apiKeyId = apiKeyMetadata?.id || null;
|
||||
const scope = await getApiKeyRequestScope(request);
|
||||
if (scope.rejection) return scope.rejection;
|
||||
const apiKeyId = scope.apiKeyId;
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const limit = Math.min(Number.parseInt(searchParams.get("limit") || "20") || 20, 10000);
|
||||
@@ -89,12 +94,12 @@ export async function GET(request: Request) {
|
||||
purpose,
|
||||
limit: limit + 1,
|
||||
after,
|
||||
order
|
||||
order,
|
||||
});
|
||||
|
||||
const hasMore = files.length > limit;
|
||||
const data = files.slice(0, limit);
|
||||
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
object: "list",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
:root {
|
||||
--desktop-safe-top: 0px;
|
||||
--desktop-safe-bottom: 0px;
|
||||
color-scheme: light;
|
||||
|
||||
/* Primary - Coral Red (OpenClaw) */
|
||||
--color-primary: #e54d5e;
|
||||
@@ -54,6 +55,7 @@
|
||||
|
||||
.dark {
|
||||
/* Dark theme (ClawHub deep) */
|
||||
color-scheme: dark;
|
||||
--color-bg: #0b0e14;
|
||||
--color-bg-alt: #111520;
|
||||
--color-bg-primary: #0b0e14;
|
||||
|
||||
5
src/lib/api/requireCliToolsAuth.ts
Normal file
5
src/lib/api/requireCliToolsAuth.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
export async function requireCliToolsAuth(request: Request): Promise<Response | null> {
|
||||
return requireManagementAuth(request);
|
||||
}
|
||||
49
src/lib/batches/dispatch.ts
Normal file
49
src/lib/batches/dispatch.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { SupportedBatchEndpoint } from "@/shared/constants/batchEndpoints";
|
||||
|
||||
type BatchRouteHandler = (request: Request) => Promise<Response> | Response;
|
||||
|
||||
const handlerLoaders: Record<SupportedBatchEndpoint, () => Promise<BatchRouteHandler>> = {
|
||||
"/v1/responses": async () => (await import("@/app/api/v1/responses/route")).POST,
|
||||
"/v1/chat/completions": async () => (await import("@/app/api/v1/chat/completions/route")).POST,
|
||||
"/v1/embeddings": async () => (await import("@/app/api/v1/embeddings/route")).POST,
|
||||
"/v1/completions": async () => (await import("@/app/api/v1/completions/route")).POST,
|
||||
"/v1/moderations": async () => (await import("@/app/api/v1/moderations/route")).POST,
|
||||
"/v1/images/generations": async () =>
|
||||
(await import("@/app/api/v1/images/generations/route")).POST,
|
||||
"/v1/videos/generations": async () =>
|
||||
(await import("@/app/api/v1/videos/generations/route")).POST,
|
||||
};
|
||||
|
||||
const handlerCache = new Map<SupportedBatchEndpoint, BatchRouteHandler>();
|
||||
|
||||
async function getHandler(endpoint: SupportedBatchEndpoint): Promise<BatchRouteHandler> {
|
||||
const cached = handlerCache.get(endpoint);
|
||||
if (cached) return cached;
|
||||
|
||||
const handler = await handlerLoaders[endpoint]();
|
||||
handlerCache.set(endpoint, handler);
|
||||
return handler;
|
||||
}
|
||||
|
||||
export async function dispatchBatchApiRequest({
|
||||
endpoint,
|
||||
body,
|
||||
apiKey,
|
||||
}: {
|
||||
endpoint: SupportedBatchEndpoint;
|
||||
body: Record<string, unknown>;
|
||||
apiKey?: string | null;
|
||||
}): Promise<Response> {
|
||||
const headers = new Headers({ "Content-Type": "application/json" });
|
||||
if (apiKey) {
|
||||
headers.set("Authorization", `Bearer ${apiKey}`);
|
||||
}
|
||||
|
||||
const handler = await getHandler(endpoint);
|
||||
const request = new Request(`http://localhost${endpoint}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return await handler(request);
|
||||
}
|
||||
@@ -4,13 +4,25 @@ import { v4 as uuidv4 } from "uuid";
|
||||
function parseBatchRow(row: any): BatchRecord {
|
||||
const camel = rowToCamel(row) as any;
|
||||
if (camel.metadata && typeof camel.metadata === "string") {
|
||||
try { camel.metadata = JSON.parse(camel.metadata); } catch { camel.metadata = null; }
|
||||
try {
|
||||
camel.metadata = JSON.parse(camel.metadata);
|
||||
} catch {
|
||||
camel.metadata = null;
|
||||
}
|
||||
}
|
||||
if (camel.errors && typeof camel.errors === "string") {
|
||||
try { camel.errors = JSON.parse(camel.errors); } catch { camel.errors = null; }
|
||||
try {
|
||||
camel.errors = JSON.parse(camel.errors);
|
||||
} catch {
|
||||
camel.errors = null;
|
||||
}
|
||||
}
|
||||
if (camel.usage && typeof camel.usage === "string") {
|
||||
try { camel.usage = JSON.parse(camel.usage); } catch { camel.usage = null; }
|
||||
try {
|
||||
camel.usage = JSON.parse(camel.usage);
|
||||
} catch {
|
||||
camel.usage = null;
|
||||
}
|
||||
}
|
||||
return camel as BatchRecord;
|
||||
}
|
||||
@@ -19,7 +31,15 @@ export interface BatchRecord {
|
||||
id: string;
|
||||
endpoint: string;
|
||||
completionWindow: string;
|
||||
status: "validating" | "failed" | "in_progress" | "finalizing" | "completed" | "expired" | "cancelling" | "cancelled";
|
||||
status:
|
||||
| "validating"
|
||||
| "failed"
|
||||
| "in_progress"
|
||||
| "finalizing"
|
||||
| "completed"
|
||||
| "expired"
|
||||
| "cancelling"
|
||||
| "cancelled";
|
||||
inputFileId: string;
|
||||
outputFileId?: string | null;
|
||||
errorFileId?: string | null;
|
||||
@@ -44,7 +64,17 @@ export interface BatchRecord {
|
||||
outputExpiresAfterAnchor?: string | null;
|
||||
}
|
||||
|
||||
export function createBatch(batch: Omit<BatchRecord, "id" | "createdAt" | "status" | "requestCountsTotal" | "requestCountsCompleted" | "requestCountsFailed">): BatchRecord {
|
||||
export function createBatch(
|
||||
batch: Omit<
|
||||
BatchRecord,
|
||||
| "id"
|
||||
| "createdAt"
|
||||
| "status"
|
||||
| "requestCountsTotal"
|
||||
| "requestCountsCompleted"
|
||||
| "requestCountsFailed"
|
||||
>
|
||||
): BatchRecord {
|
||||
const db = getDbInstance();
|
||||
const id = "batch_" + uuidv4().replaceAll("-", "").substring(0, 24);
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
@@ -67,15 +97,13 @@ export function createBatch(batch: Omit<BatchRecord, "id" | "createdAt" | "statu
|
||||
...record,
|
||||
metadata: record.metadata ? JSON.stringify(record.metadata) : null,
|
||||
errors: record.errors ? JSON.stringify(record.errors) : null,
|
||||
usage: record.usage ? JSON.stringify(record.usage) : null
|
||||
usage: record.usage ? JSON.stringify(record.usage) : null,
|
||||
}) as any;
|
||||
const keys = Object.keys(snakeRecord);
|
||||
const values = Object.values(snakeRecord);
|
||||
const placeholders = keys.map(() => "?").join(", ");
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO batches (${keys.join(", ")}) VALUES (${placeholders})`
|
||||
).run(...values);
|
||||
db.prepare(`INSERT INTO batches (${keys.join(", ")}) VALUES (${placeholders})`).run(...values);
|
||||
|
||||
return record;
|
||||
}
|
||||
@@ -99,52 +127,61 @@ export function updateBatch(id: string, updates: Partial<BatchRecord>): boolean
|
||||
if (snakeUpdates.usage && typeof snakeUpdates.usage !== "string") {
|
||||
snakeUpdates.usage = JSON.stringify(snakeUpdates.usage);
|
||||
}
|
||||
|
||||
|
||||
const keys = Object.keys(snakeUpdates);
|
||||
if (keys.length === 0) return false;
|
||||
|
||||
const setClause = keys.map(k => `${k} = ?`).join(", ");
|
||||
|
||||
const setClause = keys.map((k) => `${k} = ?`).join(", ");
|
||||
const values = Object.values(snakeUpdates);
|
||||
|
||||
|
||||
const result = db.prepare(`UPDATE batches SET ${setClause} WHERE id = ?`).run(...values, id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
export function listBatches(apiKeyId?: string, limit: number = 20, after?: string): BatchRecord[] {
|
||||
const db = getDbInstance();
|
||||
const afterBatch = after ? getBatch(after) : null;
|
||||
let rows: any[];
|
||||
if (apiKeyId) {
|
||||
if (after) {
|
||||
if (afterBatch) {
|
||||
rows = db
|
||||
.prepare("SELECT * FROM batches WHERE api_key_id = ? AND id < ? ORDER BY id DESC LIMIT ?")
|
||||
.all(apiKeyId, after, limit);
|
||||
.prepare(
|
||||
"SELECT * FROM batches WHERE api_key_id = ? AND (created_at < ? OR (created_at = ? AND id < ?)) ORDER BY created_at DESC, id DESC LIMIT ?"
|
||||
)
|
||||
.all(apiKeyId, afterBatch.createdAt, afterBatch.createdAt, after, limit);
|
||||
} else {
|
||||
rows = db
|
||||
.prepare("SELECT * FROM batches WHERE api_key_id = ? ORDER BY id DESC LIMIT ?")
|
||||
.prepare(
|
||||
"SELECT * FROM batches WHERE api_key_id = ? ORDER BY created_at DESC, id DESC LIMIT ?"
|
||||
)
|
||||
.all(apiKeyId, limit);
|
||||
}
|
||||
} else if (after) {
|
||||
} else if (afterBatch) {
|
||||
rows = db
|
||||
.prepare("SELECT * FROM batches WHERE id < ? ORDER BY id DESC LIMIT ?")
|
||||
.all(after, limit);
|
||||
.prepare(
|
||||
"SELECT * FROM batches WHERE (created_at < ? OR (created_at = ? AND id < ?)) ORDER BY created_at DESC, id DESC LIMIT ?"
|
||||
)
|
||||
.all(afterBatch.createdAt, afterBatch.createdAt, after, limit);
|
||||
} else {
|
||||
rows = db.prepare("SELECT * FROM batches ORDER BY id DESC LIMIT ?").all(limit);
|
||||
rows = db.prepare("SELECT * FROM batches ORDER BY created_at DESC, id DESC LIMIT ?").all(limit);
|
||||
}
|
||||
return rows.map(row => parseBatchRow(row));
|
||||
return rows.map((row) => parseBatchRow(row));
|
||||
}
|
||||
|
||||
export function getPendingBatches(): BatchRecord[] {
|
||||
const db = getDbInstance();
|
||||
const rows = db.prepare(
|
||||
"SELECT * FROM batches WHERE status IN ('validating', 'in_progress', 'cancelling')"
|
||||
).all();
|
||||
return rows.map(row => parseBatchRow(row));
|
||||
const rows = db
|
||||
.prepare("SELECT * FROM batches WHERE status IN ('validating', 'in_progress', 'cancelling')")
|
||||
.all();
|
||||
return rows.map((row) => parseBatchRow(row));
|
||||
}
|
||||
|
||||
export function getTerminalBatches(): BatchRecord[] {
|
||||
const db = getDbInstance();
|
||||
const rows = db.prepare(
|
||||
"SELECT * FROM batches WHERE status IN ('completed', 'failed', 'cancelled', 'expired') ORDER BY created_at ASC"
|
||||
).all();
|
||||
return rows.map(row => parseBatchRow(row));
|
||||
const rows = db
|
||||
.prepare(
|
||||
"SELECT * FROM batches WHERE status IN ('completed', 'failed', 'cancelled', 'expired') ORDER BY created_at ASC"
|
||||
)
|
||||
.all();
|
||||
return rows.map((row) => parseBatchRow(row));
|
||||
}
|
||||
|
||||
11
src/shared/constants/batchEndpoints.ts
Normal file
11
src/shared/constants/batchEndpoints.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const SUPPORTED_BATCH_ENDPOINTS = [
|
||||
"/v1/responses",
|
||||
"/v1/chat/completions",
|
||||
"/v1/embeddings",
|
||||
"/v1/completions",
|
||||
"/v1/moderations",
|
||||
"/v1/images/generations",
|
||||
"/v1/videos/generations",
|
||||
] as const;
|
||||
|
||||
export type SupportedBatchEndpoint = (typeof SUPPORTED_BATCH_ENDPOINTS)[number];
|
||||
@@ -15,6 +15,7 @@ const CLI_TOOLS: Record<string, any> = {
|
||||
healthcheckTimeoutMs: 4000,
|
||||
paths: {
|
||||
settings: ".claude/settings.json",
|
||||
auth: ".claude/.credentials.json",
|
||||
},
|
||||
},
|
||||
codex: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { SUPPORTED_BATCH_ENDPOINTS } from "@/shared/constants/batchEndpoints";
|
||||
import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility";
|
||||
import { isForbiddenUpstreamHeaderName } from "@/shared/constants/upstreamHeaders";
|
||||
|
||||
@@ -1799,16 +1800,7 @@ export const versionManagerInstallSchema = versionManagerToolSchema.extend({
|
||||
|
||||
export const v1BatchCreateSchema = z.object({
|
||||
input_file_id: z.string().min(1),
|
||||
endpoint: z.enum([
|
||||
"/v1/responses",
|
||||
"/v1/chat/completions",
|
||||
"/v1/embeddings",
|
||||
"/v1/completions",
|
||||
"/v1/moderations",
|
||||
"/v1/images/generations",
|
||||
"/v1/images/edits",
|
||||
"/v1/videos",
|
||||
]),
|
||||
endpoint: z.enum(SUPPORTED_BATCH_ENDPOINTS),
|
||||
completion_window: z.enum(["24h"]),
|
||||
metadata: z
|
||||
.record(z.string().max(64), z.string().max(512))
|
||||
|
||||
@@ -699,6 +699,8 @@ async function handleSingleModelChat(
|
||||
// 4. Execute chat via core after breaker gate checks (with optional TLS tracking)
|
||||
if (telemetry) telemetry.startPhase("connect");
|
||||
const { result, tlsFingerprintUsed } = await executeChatWithBreaker({
|
||||
bypassCircuitBreaker: forceLiveComboTest || hasForcedConnection,
|
||||
breaker,
|
||||
body: requestBody,
|
||||
provider,
|
||||
model,
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
isTlsFingerprintActive,
|
||||
} from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
import { resolveProxyForConnection } from "@/lib/localDb";
|
||||
import { getCircuitBreaker } from "../../shared/utils/circuitBreaker";
|
||||
import { CircuitBreakerOpenError, getCircuitBreaker } from "../../shared/utils/circuitBreaker";
|
||||
import { logProxyEvent } from "../../lib/proxyLogger";
|
||||
import { logTranslationEvent } from "../../lib/translatorEvents";
|
||||
import { getRuntimeProviderProfile } from "@omniroute/open-sse/services/accountFallback.ts";
|
||||
@@ -102,6 +102,8 @@ export async function checkPipelineGates(
|
||||
}
|
||||
|
||||
export async function executeChatWithBreaker({
|
||||
bypassCircuitBreaker,
|
||||
breaker,
|
||||
body,
|
||||
provider,
|
||||
model,
|
||||
@@ -154,14 +156,36 @@ export async function executeChatWithBreaker({
|
||||
})
|
||||
);
|
||||
|
||||
if (bypassCircuitBreaker) {
|
||||
if (!proxyInfo?.proxy && isTlsFingerprintActive()) {
|
||||
const tracked = await runWithTlsTracking(chatFn);
|
||||
return { result: tracked.result, tlsFingerprintUsed: tracked.tlsFingerprintUsed };
|
||||
}
|
||||
|
||||
const result = await chatFn();
|
||||
return { result, tlsFingerprintUsed: false };
|
||||
}
|
||||
|
||||
if (!proxyInfo?.proxy && isTlsFingerprintActive()) {
|
||||
const tracked = await runWithTlsTracking(chatFn);
|
||||
const tracked = await breaker.execute(async () => runWithTlsTracking(chatFn));
|
||||
return { result: tracked.result, tlsFingerprintUsed: tracked.tlsFingerprintUsed };
|
||||
}
|
||||
|
||||
const result = await chatFn();
|
||||
const result = await breaker.execute(chatFn);
|
||||
return { result, tlsFingerprintUsed: false };
|
||||
} catch (cbErr: any) {
|
||||
if (cbErr instanceof CircuitBreakerOpenError) {
|
||||
log.warn("CIRCUIT", `${provider} circuit open during retry: ${cbErr.message}`);
|
||||
return {
|
||||
result: {
|
||||
success: false,
|
||||
response: providerCircuitOpenResponse(provider, Math.ceil(cbErr.retryAfterMs / 1000)),
|
||||
status: HTTP_STATUS.SERVICE_UNAVAILABLE,
|
||||
},
|
||||
tlsFingerprintUsed: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (cbErr?.code === "PROXY_UNREACHABLE" || /proxy unreachable/i.test(cbErr?.message || "")) {
|
||||
const detail = cbErr?.message || "Proxy unreachable";
|
||||
log.warn("PROXY", detail);
|
||||
|
||||
Reference in New Issue
Block a user