mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
chore: apply PR 1495 and update changelog
This commit is contained in:
52
CHANGELOG.md
52
CHANGELOG.md
@@ -8,17 +8,61 @@
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430)
|
||||
- **feat(providers):** Add ModelScope provider (Chinese AI marketplace) with Kimi K2.5, GLM-5, and Step-3.5-Flash integration. (#1430 — thanks @clousky2020)
|
||||
- **feat(providers):** Add LM Studio as an OpenAI-compatible local provider for self-hosted model inference.
|
||||
- **feat(providers):** Add Grok 4.3 thinking model support for xAI web executor requests.
|
||||
- **feat(core):** Implement provider-level Circuit Breaker to prevent cascading failures across connections, enforcing a 10-minute cooldown after 5 consecutive transient failures. (#1430)
|
||||
- **feat(core):** Add daily quota exhaustion lock to detect "quota exceeded" signals and lock the specific model until midnight. (#1430)
|
||||
- **feat(core):** Auto-inject `stream_options.include_usage = true` for OpenAI format streams to guarantee token usage is reported correctly during streaming. (#1423)
|
||||
- **feat(core):** Add OpenAI Batch Processing API support — submit, monitor, and manage batch jobs through the proxy with full lifecycle tracking.
|
||||
- **feat(vision-bridge):** Add automatic image description fallback for non-vision models via `VisionBridgeGuardrail` (priority 5). Intercepts image-bearing requests to non-vision models, extracts descriptions via a configurable vision model (default: gpt-4o-mini), and replaces images with text before forwarding. Fails open on any error. (#1476)
|
||||
- **feat(dashboard):** Introduce real-time model status badges with countdown timers in the provider detail and combo panel interfaces. (#1430)
|
||||
- **feat(dashboard):** Add Batch/File management data grid to the Dashboard. (#1479)
|
||||
- **feat(dashboard):** Add Batch/File management data grid with full i18n translations for batch processing workflows. (#1479)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438)
|
||||
- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization) (#163, #164)
|
||||
- **fix(batch):** Add batch item dispatching to specific handlers based on URL to support embeddings and other modalities (#1495 — thanks @hartmark)
|
||||
- **fix(dashboard):** Correct TOML round-trip corruption in Codex config serializer by dequoting keys and preserving array/boolean structures properly. (#1438 — thanks @benzntech)
|
||||
- **fix(security):** Resolve CodeQL alert 164 (ReDoS in extraction) and 163 (incomplete URL sanitization). (#163, #164)
|
||||
- **fix(providers):** Add optional chaining to connection object before accessing `providerSpecificData`, preventing runtime errors when the connection is null/undefined.
|
||||
- **fix(codex):** Preserve namespace MCP tools forwarded to Codex Responses API, preventing tool name stripping during translation. (#1483)
|
||||
- **fix(codex):** Deduplicate case-variant `anthropic-version` header in Claude Code patch to prevent duplicate header injection. (#1481)
|
||||
- **fix(fallback):** Use shared `CircuitBreaker` instead of undefined constants, fixing runtime errors in provider failure handling. (#1485)
|
||||
- **fix(fallback):** Merge new provider failure threshold fields (`providerFailureThreshold`, `providerFailureWindowMs`, `providerCooldownMs`) into resilience profiles.
|
||||
- **fix(fallback):** Remove 429 from `PROVIDER_FAILURE_ERROR_CODES` — rate limits are already handled by model-level and account-level locks; including them in the provider-wide circuit breaker caused premature cooldown.
|
||||
- **fix(sse):** Enable tool calling for GPT OSS and DeepSeek Reasoner models. (#1455)
|
||||
- **fix(encryption):** Return null on decryption failure to prevent sending encrypted tokens to providers. (#1462)
|
||||
- **fix(combo):** Resolve cross-provider thinking 400 errors and HTTP clipboard issues during combo routing. (#1444)
|
||||
- **fix(core):** Resolve skills, memory, and encryption system issues affecting startup and runtime stability. (#1456)
|
||||
- **fix(core):** Fix model ID parsing for providers with slashes in model names — use `indexOf`/`substring` instead of `split` to handle models like `modelscope/moonshotai/Kimi-K2.5`.
|
||||
- **fix(core):** Fix reference counting in `ModelStatusContext` — changed `registeredModels` from `Set` to `Map<string, number>` to prevent polling stop when one component unmounts while others still track the same model.
|
||||
- **fix(security):** Prompt injection guard failures now return an explicit 500 response instead of silently passing through (fail-closed policy).
|
||||
- **fix(security):** Encryption now derives new keys from a secret-based salt while falling back to the legacy static-salt key during decryption, preserving existing stored credentials.
|
||||
|
||||
### ♻️ Refactoring
|
||||
|
||||
- **refactor(fallback):** Make provider failure thresholds configurable via `PROVIDER_PROFILES` instead of hardcoded constants, supporting different failure tolerance per provider type. (#1449)
|
||||
- **refactor(resilience):** Unify resilience controls across the codebase for consistent circuit breaker and fallback behavior. (#1449)
|
||||
- **refactor(core):** Implement shared path utilities, add custom date formatting, improve type safety, and unify database imports across modules.
|
||||
- **refactor(security):** Harden backup archive creation by switching to `execFileSync`, validate ACP agent IDs, expand shared CORS handling.
|
||||
|
||||
### 🧪 Tests
|
||||
|
||||
- **test(vision-bridge):** Add 51 unit tests covering all VisionBridge spec scenarios (VB-S01 through VB-S10), including helper functions for `callVisionModel`, `extractImageParts`, `replaceImageParts`, and `resolveImageAsDataUri`.
|
||||
- **test(batch-api):** Isolate batch API unit tests with temp `DATA_DIR` to prevent schema state collisions.
|
||||
- **test(settings-api):** Add test harness with `createSettingsApiHarness` function for proper temp directory setup and storage reset between tests.
|
||||
- **test(security):** Update prompt injection test for fail-closed policy alignment.
|
||||
- **test(core):** Restore local test fixes for encryption and resilience modules.
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- **docs:** Add Arch Linux AUR install notes for community package support. (#1478)
|
||||
- **docs(i18n):** Improve Ukrainian (uk-UA) translation quality — full Ukrainian translation for README, SECURITY, A2A-SERVER, API_REFERENCE, AUTO-COMBO, and USER_GUIDE documents. Fix mixed Latin/Cyrillic typos, translate model table entries, and standardize section headers.
|
||||
|
||||
### 📦 Dependencies
|
||||
|
||||
- **deps:** Bump the development group with 4 updates. (#1464)
|
||||
- **deps:** Bump the production group with 4 updates. (#1463)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
"files": [
|
||||
"main.js",
|
||||
"preload.js",
|
||||
"package.json"
|
||||
"package.json",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
|
||||
@@ -339,12 +339,14 @@ function normalizeCodexTools(body: Record<string, unknown>): void {
|
||||
|
||||
// Preserve namespace tools (MCP tool groups used by Codex/OpenAI Responses API).
|
||||
// Codex API supports them natively; register sub-tool names for tool_choice validation.
|
||||
if (tool.type === "namespace" && Array.isArray(tool.tools)) {
|
||||
for (const st of tool.tools as unknown[]) {
|
||||
if (st && typeof st === "object" && !Array.isArray(st)) {
|
||||
const subTool = st as Record<string, unknown>;
|
||||
const name = typeof subTool.name === "string" ? subTool.name.trim() : "";
|
||||
if (name) validToolNames.add(name);
|
||||
if (tool.type === "namespace") {
|
||||
if (Array.isArray(tool.tools)) {
|
||||
for (const st of tool.tools as unknown[]) {
|
||||
if (st && typeof st === "object" && !Array.isArray(st)) {
|
||||
const subTool = st as Record<string, unknown>;
|
||||
const name = typeof subTool.name === "string" ? subTool.name.trim() : "";
|
||||
if (name) validToolNames.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
createFile,
|
||||
deleteFile,
|
||||
getApiKeyById,
|
||||
getBatch,
|
||||
getFileContent,
|
||||
getPendingBatches,
|
||||
getTerminalBatches,
|
||||
listFiles,
|
||||
updateBatch,
|
||||
getFileContent,
|
||||
createFile,
|
||||
getApiKeyById,
|
||||
getBatch,
|
||||
listFiles,
|
||||
deleteFile,
|
||||
updateFileStatus,
|
||||
} from "@/lib/localDb";
|
||||
import { handleChat } from "@/sse/handlers/chat";
|
||||
import { POST as handleEmbeddingsRoute } from "@/app/api/v1/embeddings/route";
|
||||
import { POST as handleModerationsRoute } from "@/app/api/v1/moderations/route";
|
||||
import { POST as handleImagesGenerationsRoute } from "@/app/api/v1/images/generations/route";
|
||||
import { POST as handleVideosGenerationsRoute } from "@/app/api/v1/videos/generations/route";
|
||||
import type { BatchRecord } from "@/lib/localDb";
|
||||
import { dispatchBatchApiRequest } from "@/lib/batches/dispatch";
|
||||
import type { SupportedBatchEndpoint } from "@/shared/constants/batchEndpoints";
|
||||
|
||||
let isProcessing = false;
|
||||
let pollInterval: NodeJS.Timeout | null = null;
|
||||
const DEFAULT_BATCH_WINDOW_SECONDS = 24 * 60 * 60;
|
||||
|
||||
interface BatchRequestItem {
|
||||
body: Record<string, unknown>;
|
||||
customId: string | null;
|
||||
lineNumber: number;
|
||||
method: "POST";
|
||||
url: SupportedBatchEndpoint;
|
||||
}
|
||||
|
||||
export function initBatchProcessor() {
|
||||
if (pollInterval) return pollInterval;
|
||||
@@ -91,39 +98,125 @@ export async function processPendingBatches() {
|
||||
await cleanupExpiredBatches();
|
||||
}
|
||||
|
||||
function parseBatchWindowSeconds(window: string | null | undefined): number {
|
||||
if (!window) return DEFAULT_BATCH_WINDOW_SECONDS;
|
||||
const match = /^(\d+)([hdm])$/.exec(window);
|
||||
if (!match) return DEFAULT_BATCH_WINDOW_SECONDS;
|
||||
|
||||
const value = Number.parseInt(match[1], 10);
|
||||
const unit = match[2];
|
||||
if (unit === "h") return value * 3600;
|
||||
if (unit === "d") return value * 86400;
|
||||
if (unit === "m") return value * 60;
|
||||
return DEFAULT_BATCH_WINDOW_SECONDS;
|
||||
}
|
||||
|
||||
function getBatchOutputExpiresAt(batch: BatchRecord): number | null {
|
||||
if (
|
||||
batch.outputExpiresAfterAnchor === "created_at" &&
|
||||
typeof batch.outputExpiresAfterSeconds === "number" &&
|
||||
batch.outputExpiresAfterSeconds > 0
|
||||
) {
|
||||
return batch.createdAt + batch.outputExpiresAfterSeconds;
|
||||
}
|
||||
|
||||
const completionTime =
|
||||
batch.completedAt || batch.failedAt || batch.cancelledAt || batch.expiredAt;
|
||||
if (!completionTime) return null;
|
||||
return completionTime + parseBatchWindowSeconds(batch.completionWindow);
|
||||
}
|
||||
|
||||
function resolveBatchApiKeyValue(batch: Pick<BatchRecord, "apiKeyId">, apiKeyRow: any) {
|
||||
if (typeof apiKeyRow?.key === "string" && apiKeyRow.key.length > 0) {
|
||||
return apiKeyRow.key;
|
||||
}
|
||||
if (batch.apiKeyId === "env-key") {
|
||||
return process.env.OMNIROUTE_API_KEY || process.env.ROUTER_API_KEY || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseBatchItems(
|
||||
content: Buffer,
|
||||
batchEndpoint: SupportedBatchEndpoint
|
||||
): { items: BatchRequestItem[]; error: null } | { items: null; error: string } {
|
||||
const lines = content
|
||||
.toString()
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const items: BatchRequestItem[] = [];
|
||||
for (const [index, line] of lines.entries()) {
|
||||
let parsed: Record<string, unknown>;
|
||||
try {
|
||||
parsed = JSON.parse(line);
|
||||
} catch {
|
||||
return { items: null, error: `Line ${index + 1} is not valid JSON` };
|
||||
}
|
||||
|
||||
const method = String(parsed.method || "POST").toUpperCase();
|
||||
const url = parsed.url;
|
||||
const body = parsed.body;
|
||||
|
||||
if (method !== "POST") {
|
||||
return {
|
||||
items: null,
|
||||
error: `Line ${index + 1} uses unsupported method ${method}; only POST is supported`,
|
||||
};
|
||||
}
|
||||
if (url !== batchEndpoint) {
|
||||
return {
|
||||
items: null,
|
||||
error: `Line ${index + 1} url ${String(url)} does not match batch endpoint ${batchEndpoint}`,
|
||||
};
|
||||
}
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
||||
return { items: null, error: `Line ${index + 1} must include a JSON object body` };
|
||||
}
|
||||
|
||||
items.push({
|
||||
body: body as Record<string, unknown>,
|
||||
customId: typeof parsed.custom_id === "string" ? parsed.custom_id : null,
|
||||
lineNumber: index + 1,
|
||||
method: "POST",
|
||||
url: batchEndpoint,
|
||||
});
|
||||
}
|
||||
|
||||
return { items, error: null };
|
||||
}
|
||||
|
||||
async function cleanupExpiredBatches() {
|
||||
try {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const batches = getTerminalBatches();
|
||||
|
||||
const parseWindow = (window: string): number => {
|
||||
if (!window) return 86400;
|
||||
const match = new RegExp(/^(\d+)([hdm])$/).exec(window);
|
||||
if (!match) return 86400;
|
||||
const val = Number.parseInt(match[1]);
|
||||
const unit = match[2];
|
||||
if (unit === "h") return val * 3600;
|
||||
if (unit === "d") return val * 86400;
|
||||
if (unit === "m") return val * 60;
|
||||
return 86400;
|
||||
};
|
||||
|
||||
// Delete files for terminal batches that have exceeded their completion window
|
||||
for (const batch of batches) {
|
||||
const windowSeconds = parseWindow(batch.completionWindow);
|
||||
const completionTime =
|
||||
batch.completedAt || batch.failedAt || batch.cancelledAt || batch.expiredAt;
|
||||
if (completionTime && now - completionTime > windowSeconds) {
|
||||
if (batch.inputFileId) deleteFile(batch.inputFileId);
|
||||
if (batch.outputFileId) deleteFile(batch.outputFileId);
|
||||
if (batch.errorFileId) deleteFile(batch.errorFileId);
|
||||
const inputExpiresAt =
|
||||
completionTime && batch.inputFileId
|
||||
? completionTime + parseBatchWindowSeconds(batch.completionWindow)
|
||||
: null;
|
||||
const outputExpiresAt = getBatchOutputExpiresAt(batch);
|
||||
|
||||
if (batch.inputFileId && inputExpiresAt && now > inputExpiresAt) {
|
||||
deleteFile(batch.inputFileId);
|
||||
}
|
||||
if (batch.outputFileId && outputExpiresAt && now > outputExpiresAt) {
|
||||
deleteFile(batch.outputFileId);
|
||||
}
|
||||
if (batch.errorFileId && outputExpiresAt && now > outputExpiresAt) {
|
||||
deleteFile(batch.errorFileId);
|
||||
}
|
||||
}
|
||||
|
||||
// Expire validating batches that have exceeded their completion window
|
||||
for (const batch of getPendingBatches()) {
|
||||
if (batch.status === "validating") {
|
||||
const windowSeconds = parseWindow(batch.completionWindow);
|
||||
const windowSeconds = parseBatchWindowSeconds(batch.completionWindow);
|
||||
if (now - batch.createdAt > windowSeconds) {
|
||||
updateBatch(batch.id, { status: "expired", expiredAt: now });
|
||||
}
|
||||
@@ -157,8 +250,13 @@ async function startBatch(batch: any) {
|
||||
}
|
||||
|
||||
try {
|
||||
const lines = content.toString().split("\n").filter((l) => l.trim());
|
||||
const total = lines.length;
|
||||
const parsedItems = parseBatchItems(content, batch.endpoint);
|
||||
if (parsedItems.error) {
|
||||
updateFileStatus(batch.inputFileId, "processed");
|
||||
failBatch(batch.id, parsedItems.error);
|
||||
return;
|
||||
}
|
||||
const total = parsedItems.items.length;
|
||||
|
||||
updateFileStatus(batch.inputFileId, "validating");
|
||||
updateBatch(batch.id, {
|
||||
@@ -169,7 +267,7 @@ async function startBatch(batch: any) {
|
||||
|
||||
// Fire-and-forget: process items in the background so the poll loop isn't blocked.
|
||||
// isProcessing prevents a second poll tick from overlapping.
|
||||
processBatchItems(batch, lines).catch((err) => {
|
||||
processBatchItems(batch, parsedItems.items).catch((err) => {
|
||||
console.error(`[BATCH] Critical error in processBatchItems for ${batch.id}:`, err);
|
||||
failBatch(batch.id, String(err));
|
||||
});
|
||||
@@ -179,50 +277,7 @@ async function startBatch(batch: any) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a single batch item to the correct handler based on the URL.
|
||||
* Each batch item may target a different API endpoint (chat, embeddings, etc.).
|
||||
*/
|
||||
async function dispatchBatchItem(
|
||||
url: string,
|
||||
headers: Headers,
|
||||
body: Record<string, unknown>
|
||||
): Promise<Response> {
|
||||
const request = new Request(`http://localhost${url}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
switch (url) {
|
||||
case "/v1/embeddings":
|
||||
return handleEmbeddingsRoute(request);
|
||||
case "/v1/moderations":
|
||||
return handleModerationsRoute(request);
|
||||
case "/v1/images/generations":
|
||||
case "/v1/images/edits":
|
||||
return handleImagesGenerationsRoute(request);
|
||||
case "/v1/videos":
|
||||
case "/v1/videos/generations":
|
||||
return handleVideosGenerationsRoute(request);
|
||||
// /v1/chat/completions, /v1/completions, /v1/responses
|
||||
default: {
|
||||
return handleChat(
|
||||
new Request(`http://localhost${url}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
...body,
|
||||
// BATCH-SPECIFIC: Force stream: false — batches don't support SSE responses
|
||||
stream: body.stream ?? false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function processBatchItems(batch: any, lines: string[]) {
|
||||
async function processBatchItems(batch: BatchRecord, items: BatchRequestItem[]) {
|
||||
const results: any[] = [];
|
||||
const errors: any[] = [];
|
||||
let completedCount = 0;
|
||||
@@ -233,8 +288,9 @@ async function processBatchItems(batch: any, lines: string[]) {
|
||||
let usedModel = batch.model || null;
|
||||
|
||||
const apiKeyRow = batch.apiKeyId ? await getApiKeyById(batch.apiKeyId) : null;
|
||||
const apiKeyValue = resolveBatchApiKeyValue(batch, apiKeyRow);
|
||||
|
||||
for (const line of lines) {
|
||||
for (const item of items) {
|
||||
// Check if cancelled mid-process
|
||||
const current = getBatch(batch.id);
|
||||
if (!current || current.status === "cancelling" || current.status === "cancelled") {
|
||||
@@ -242,16 +298,13 @@ async function processBatchItems(batch: any, lines: string[]) {
|
||||
}
|
||||
|
||||
try {
|
||||
const item = JSON.parse(line);
|
||||
const { custom_id: customId, url, body } = item;
|
||||
|
||||
const headers = new Headers();
|
||||
if (apiKeyRow?.key) {
|
||||
headers.set("Authorization", `Bearer ${apiKeyRow.key}`);
|
||||
}
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
const response = await dispatchBatchItem(url, headers, body);
|
||||
// BATCH-SPECIFIC: Force stream: false — batches don't support SSE responses
|
||||
const batchItemBody = { ...item.body, stream: false };
|
||||
const response = await dispatchBatchApiRequest({
|
||||
endpoint: item.url,
|
||||
body: batchItemBody,
|
||||
apiKey: apiKeyValue,
|
||||
});
|
||||
|
||||
let responseData: { error: any; id?: any; usage?: any; model?: any };
|
||||
let statusCode = 200;
|
||||
@@ -266,7 +319,9 @@ async function processBatchItems(batch: any, lines: string[]) {
|
||||
try {
|
||||
responseData = JSON.parse(text);
|
||||
} catch {
|
||||
responseData = { error: { message: text || "Unknown error", type: "invalid_response" } };
|
||||
responseData = {
|
||||
error: { message: text || "Unknown error", type: "invalid_response" },
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -278,7 +333,7 @@ async function processBatchItems(batch: any, lines: string[]) {
|
||||
|
||||
results.push({
|
||||
id: requestId,
|
||||
custom_id: customId,
|
||||
custom_id: item.customId,
|
||||
response: {
|
||||
status_code: statusCode,
|
||||
request_id: responseData?.id || "req_unknown",
|
||||
@@ -303,14 +358,8 @@ async function processBatchItems(batch: any, lines: string[]) {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[BATCH] Item failed in ${batch.id}:`, err);
|
||||
let customId = "unknown";
|
||||
try {
|
||||
customId = JSON.parse(line).custom_id || "unknown";
|
||||
} catch {
|
||||
// line was malformed JSON
|
||||
}
|
||||
errors.push({
|
||||
custom_id: customId,
|
||||
custom_id: item.customId || `line-${item.lineNumber}`,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
failedCount++;
|
||||
@@ -365,9 +414,8 @@ async function finalizeBatch(batchId: string, results: any[], itemsWithErrors: a
|
||||
}
|
||||
|
||||
let outputFileId: string | null = null;
|
||||
const successes = results.filter(
|
||||
(r) => r.response.status_code < 400 && !r.response.body?.error
|
||||
);
|
||||
const outputExpiresAt = current ? getBatchOutputExpiresAt(current) : null;
|
||||
const successes = results.filter((r) => r.response.status_code < 400 && !r.response.body?.error);
|
||||
if (successes.length > 0) {
|
||||
const outputContent = successes.map((r) => JSON.stringify(r)).join("\n");
|
||||
const file = createFile({
|
||||
@@ -377,14 +425,13 @@ async function finalizeBatch(batchId: string, results: any[], itemsWithErrors: a
|
||||
content: Buffer.from(outputContent),
|
||||
apiKeyId: current?.apiKeyId,
|
||||
status: "completed",
|
||||
expiresAt: outputExpiresAt,
|
||||
});
|
||||
outputFileId = file.id;
|
||||
}
|
||||
|
||||
let errorFileId: string | null = null;
|
||||
const failures = results.filter(
|
||||
(r) => r.response.status_code >= 400 || r.response.body?.error
|
||||
);
|
||||
const failures = results.filter((r) => r.response.status_code >= 400 || r.response.body?.error);
|
||||
const allFailures = [
|
||||
...failures,
|
||||
...itemsWithErrors.map((e) => ({
|
||||
@@ -404,6 +451,7 @@ async function finalizeBatch(batchId: string, results: any[], itemsWithErrors: a
|
||||
content: Buffer.from(errorContent),
|
||||
apiKeyId: current?.apiKeyId,
|
||||
status: "completed",
|
||||
expiresAt: outputExpiresAt,
|
||||
});
|
||||
errorFileId = file.id;
|
||||
}
|
||||
|
||||
@@ -1982,7 +1982,7 @@ async function handleRoundRobinCombo({
|
||||
}
|
||||
}
|
||||
|
||||
if (isProviderBreakerOpenResponse(result, errorBody)) {
|
||||
if (isProviderBreakerOpenResponse(result, errorBody as any)) {
|
||||
lastError = errorText || String(result.status);
|
||||
if (!lastStatus) lastStatus = result.status;
|
||||
if (offset > 0) fallbackCount++;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,14 +4,14 @@ test.describe("API Health Checks", () => {
|
||||
test("GET /api/monitoring/health returns OK", async ({ request }) => {
|
||||
const res = await request.get("/api/monitoring/health");
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const body = await res.json();
|
||||
const body = (await res.json()) as any;
|
||||
expect(body).toHaveProperty("status");
|
||||
});
|
||||
|
||||
test("GET /api/v1/models returns model list", async ({ request }) => {
|
||||
const res = await request.get("/api/v1/models");
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const body = await res.json();
|
||||
const body = (await res.json()) as any;
|
||||
expect(body).toHaveProperty("data");
|
||||
expect(Array.isArray(body.data)).toBe(true);
|
||||
});
|
||||
@@ -20,7 +20,7 @@ test.describe("API Health Checks", () => {
|
||||
const res = await request.get("/api/providers");
|
||||
// In CI with auth enabled, 401 is acceptable — endpoint is reachable
|
||||
if (res.ok()) {
|
||||
const body = await res.json();
|
||||
const body = (await res.json()) as any;
|
||||
expect(body).toHaveProperty("connections");
|
||||
expect(Array.isArray(body.connections)).toBe(true);
|
||||
} else {
|
||||
|
||||
@@ -38,21 +38,21 @@ describe("E2E: MCP Server (16 tools)", () => {
|
||||
itCase("should respond to health check", async () => {
|
||||
const res = await apiFetch("/api/monitoring/health");
|
||||
expect(res.ok).toBe(true);
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as any;
|
||||
expect(data).toHaveProperty("status");
|
||||
});
|
||||
|
||||
itCase("should list combos", async () => {
|
||||
const res = await apiFetch("/api/combos");
|
||||
expect(res.ok).toBe(true);
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as any;
|
||||
expect(Array.isArray(data?.combos)).toBe(true);
|
||||
});
|
||||
|
||||
itCase("should return quota data", async () => {
|
||||
const res = await apiFetch("/api/usage/quota");
|
||||
expect(res.ok).toBe(true);
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as any;
|
||||
expect(Array.isArray(data?.providers)).toBe(true);
|
||||
expect(data).toHaveProperty("meta");
|
||||
});
|
||||
@@ -74,7 +74,7 @@ describe("E2E: Quota Contract (/api/usage/quota)", () => {
|
||||
const res = await apiFetch("/api/usage/quota");
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as any;
|
||||
expect(Array.isArray(data.providers)).toBe(true);
|
||||
expect(data).toHaveProperty("meta");
|
||||
expect(typeof data.meta.generatedAt).toBe("string");
|
||||
@@ -96,13 +96,13 @@ describe("E2E: Quota Contract (/api/usage/quota)", () => {
|
||||
itCase("should filter quota by provider", async () => {
|
||||
const allRes = await apiFetch("/api/usage/quota");
|
||||
expect(allRes.ok).toBe(true);
|
||||
const allData = await allRes.json();
|
||||
const allData = (await allRes.json()) as any;
|
||||
if (!Array.isArray(allData.providers) || allData.providers.length === 0) return;
|
||||
|
||||
const provider = allData.providers[0].provider;
|
||||
const filteredRes = await apiFetch(`/api/usage/quota?provider=${encodeURIComponent(provider)}`);
|
||||
expect(filteredRes.ok).toBe(true);
|
||||
const filteredData = await filteredRes.json();
|
||||
const filteredData = (await filteredRes.json()) as any;
|
||||
expect(filteredData.meta.filters.provider).toBe(provider);
|
||||
expect(Array.isArray(filteredData.providers)).toBe(true);
|
||||
expect(filteredData.providers.every((p: any) => p.provider === provider)).toBe(true);
|
||||
@@ -111,7 +111,7 @@ describe("E2E: Quota Contract (/api/usage/quota)", () => {
|
||||
itCase("should filter quota by connectionId", async () => {
|
||||
const allRes = await apiFetch("/api/usage/quota");
|
||||
expect(allRes.ok).toBe(true);
|
||||
const allData = await allRes.json();
|
||||
const allData = (await allRes.json()) as any;
|
||||
if (!Array.isArray(allData.providers) || allData.providers.length === 0) return;
|
||||
|
||||
const connectionId = allData.providers[0].connectionId;
|
||||
@@ -119,7 +119,7 @@ describe("E2E: Quota Contract (/api/usage/quota)", () => {
|
||||
`/api/usage/quota?connectionId=${encodeURIComponent(connectionId)}`
|
||||
);
|
||||
expect(filteredRes.ok).toBe(true);
|
||||
const filteredData = await filteredRes.json();
|
||||
const filteredData = (await filteredRes.json()) as any;
|
||||
expect(filteredData.meta.filters.connectionId).toBe(connectionId);
|
||||
expect(Array.isArray(filteredData.providers)).toBe(true);
|
||||
expect(filteredData.providers.every((p: any) => p.connectionId === connectionId)).toBe(true);
|
||||
@@ -131,7 +131,7 @@ describe("E2E: A2A Server (lifecycle)", () => {
|
||||
itCase("should serve Agent Card", async () => {
|
||||
const res = await apiFetch("/.well-known/agent.json");
|
||||
expect(res.ok).toBe(true);
|
||||
const card = await res.json();
|
||||
const card = (await res.json()) as any;
|
||||
expect(card).toHaveProperty("name");
|
||||
expect(card).toHaveProperty("skills");
|
||||
expect(card).toHaveProperty("version");
|
||||
@@ -152,7 +152,7 @@ describe("E2E: A2A Server (lifecycle)", () => {
|
||||
}),
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as any;
|
||||
expect(data).toHaveProperty("result");
|
||||
expect(data.result.task).toHaveProperty("id");
|
||||
expect(data.result.task).toHaveProperty("state");
|
||||
@@ -168,7 +168,7 @@ describe("E2E: A2A Server (lifecycle)", () => {
|
||||
params: {},
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as any;
|
||||
expect(data).toHaveProperty("error");
|
||||
expect(data.error.code).toBe(-32601);
|
||||
});
|
||||
@@ -197,7 +197,7 @@ describe("E2E: Auto-Combo (routing + self-healing)", () => {
|
||||
const res = await apiFetch("/api/combos");
|
||||
if (!res.ok) console.error("GET /api/combos failed:", await res.text());
|
||||
expect(res.ok).toBe(true);
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as any;
|
||||
expect(Array.isArray(data?.combos)).toBe(true);
|
||||
const autoCombos = data.combos.filter((c: any) => c.strategy === "auto");
|
||||
expect(autoCombos.length).toBeGreaterThanOrEqual(0);
|
||||
@@ -209,7 +209,7 @@ describe("E2E: OpenClaw Integration", () => {
|
||||
itCase("should return dynamic provider.order", async () => {
|
||||
const res = await apiFetch("/api/cli-tools/openclaw/auto-order");
|
||||
expect(res.ok).toBe(true);
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as any;
|
||||
expect(data).toHaveProperty("provider");
|
||||
expect(data.provider).toHaveProperty("order");
|
||||
expect(Array.isArray(data.provider.order)).toBe(true);
|
||||
@@ -306,7 +306,7 @@ describe("E2E: Security", () => {
|
||||
method: "POST",
|
||||
body: "not-json",
|
||||
});
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as any;
|
||||
if (data.error) {
|
||||
expect(data.error.code).toBe(-32700); // Parse error
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ async function getDashboardAuthState(page: Page) {
|
||||
return await page.evaluate(async () => {
|
||||
const safeJson = async (response: Response) => {
|
||||
try {
|
||||
return await response.json();
|
||||
return (await response.json()) as any;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export async function gotoDashboardRoute(
|
||||
|
||||
await page.locator("body").waitFor({ state: "visible", timeout: timeoutMs });
|
||||
return;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("Protocol clients E2E", () => {
|
||||
expect([200, 401]).toContain(auditRes.status);
|
||||
if (auditRes.status === 200) {
|
||||
expect(auditRes.ok).toBe(true);
|
||||
const auditJson = await auditRes.json();
|
||||
const auditJson = (await auditRes.json()) as any;
|
||||
const entries = Array.isArray(auditJson?.entries) ? auditJson.entries : [];
|
||||
expect(entries.some((entry: any) => entry.toolName === "omniroute_get_health")).toBe(true);
|
||||
}
|
||||
@@ -142,7 +142,7 @@ describe("Protocol clients E2E", () => {
|
||||
async () => {
|
||||
const cardRes = await apiFetch("/.well-known/agent.json");
|
||||
expect(cardRes.ok).toBe(true);
|
||||
const card = await cardRes.json();
|
||||
const card = (await cardRes.json()) as any;
|
||||
expect(card).toHaveProperty("name");
|
||||
expect(Array.isArray(card?.skills)).toBe(true);
|
||||
|
||||
@@ -203,7 +203,7 @@ describe("Protocol clients E2E", () => {
|
||||
expect([200, 401]).toContain(tasksRes.status);
|
||||
if (tasksRes.status === 200) {
|
||||
expect(tasksRes.ok).toBe(true);
|
||||
const tasksJson = await tasksRes.json();
|
||||
const tasksJson = (await tasksRes.json()) as any;
|
||||
const tasks = Array.isArray(tasksJson?.tasks) ? tasksJson.tasks : [];
|
||||
expect(tasks.some((task: any) => task.id === sendTaskId)).toBe(true);
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@ test("API keys routes require management auth when login protection is enabled",
|
||||
})
|
||||
);
|
||||
|
||||
const unauthenticatedBody = await unauthenticated.json();
|
||||
const invalidTokenBody = await invalidToken.json();
|
||||
const unauthenticatedBody = (await unauthenticated.json()) as any;
|
||||
const invalidTokenBody = (await invalidToken.json()) as any;
|
||||
|
||||
assert.equal(unauthenticated.status, 401);
|
||||
assert.equal(unauthenticatedBody.error.message, "Authentication required");
|
||||
@@ -99,8 +99,8 @@ test("API keys POST also requires management auth when login protection is enabl
|
||||
})
|
||||
);
|
||||
|
||||
const unauthenticatedBody = await unauthenticated.json();
|
||||
const invalidTokenBody = await invalidToken.json();
|
||||
const unauthenticatedBody = (await unauthenticated.json()) as any;
|
||||
const invalidTokenBody = (await invalidToken.json()) as any;
|
||||
|
||||
assert.equal(unauthenticated.status, 401);
|
||||
assert.equal(unauthenticatedBody.error.message, "Authentication required");
|
||||
@@ -117,7 +117,7 @@ test("POST /api/keys creates a key, preserves special characters, and persists n
|
||||
body: { name: "Key / Prod #1", noLog: true },
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
const stored = await apiKeysDb.getApiKeyById(body.id);
|
||||
|
||||
assert.equal(response.status, 201);
|
||||
@@ -160,7 +160,7 @@ test("POST /api/keys returns a server error for malformed JSON payloads", async
|
||||
body: "{",
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 500);
|
||||
assert.equal(body.error, "Failed to create key");
|
||||
@@ -180,8 +180,8 @@ test("GET /api/keys lists masked keys with pagination and GET /api/keys/[id] sta
|
||||
{ params: Promise.resolve({ id: createdB.id }) }
|
||||
);
|
||||
|
||||
const listBody = await listResponse.json();
|
||||
const getBody = await getResponse.json();
|
||||
const listBody = (await listResponse.json()) as any;
|
||||
const getBody = (await getResponse.json()) as any;
|
||||
|
||||
assert.equal(listResponse.status, 200);
|
||||
assert.equal(listBody.total, 3);
|
||||
@@ -205,7 +205,7 @@ test("GET /api/keys falls back to default pagination for invalid query params",
|
||||
const response = await listRoute.GET(
|
||||
await makeManagementSessionRequest("http://localhost/api/keys?limit=0&offset=-25")
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.total, 3);
|
||||
@@ -222,7 +222,7 @@ test("GET /api/keys treats non-numeric pagination params as defaults", async ()
|
||||
const response = await listRoute.GET(
|
||||
await makeManagementSessionRequest("http://localhost/api/keys?limit=abc&offset=xyz")
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.total, 3);
|
||||
@@ -243,7 +243,7 @@ test("GET /api/keys uses default pagination when query params are absent and rep
|
||||
const response = await listRoute.GET(
|
||||
await makeManagementSessionRequest("http://localhost/api/keys")
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.total, 3);
|
||||
@@ -274,7 +274,7 @@ test("POST /api/keys triggers cloud sync when cloud mode is enabled", async () =
|
||||
body: { name: "Cloud Synced Key" },
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
const syncPayload = JSON.parse(calls[0].options.body);
|
||||
|
||||
assert.equal(response.status, 201);
|
||||
@@ -309,7 +309,7 @@ test("GET /api/keys returns 500 when the key store throws unexpectedly", async (
|
||||
|
||||
try {
|
||||
const response = await listRoute.GET(new Request("http://localhost/api/keys"));
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 500);
|
||||
assert.equal(body.error, "Failed to fetch keys");
|
||||
@@ -340,7 +340,7 @@ test("POST /api/keys still succeeds when cloud sync fails after creation", async
|
||||
body: { name: "Cloud Failure Tolerated" },
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
const stored = await apiKeysDb.getApiKeyById(body.id);
|
||||
|
||||
assert.equal(response.status, 201);
|
||||
@@ -372,9 +372,9 @@ test("GET /api/keys/[id] returns 404 for an unknown key and reveal is gated by t
|
||||
{ params: Promise.resolve({ id: created.id }) }
|
||||
);
|
||||
|
||||
const missingBody = await missingResponse.json();
|
||||
const revealDisabledBody = await revealDisabled.json();
|
||||
const revealEnabledBody = await revealEnabled.json();
|
||||
const missingBody = (await missingResponse.json()) as any;
|
||||
const revealDisabledBody = (await revealDisabled.json()) as any;
|
||||
const revealEnabledBody = (await revealEnabled.json()) as any;
|
||||
|
||||
assert.equal(missingResponse.status, 404);
|
||||
assert.equal(missingBody.error, "Key not found");
|
||||
@@ -417,9 +417,9 @@ test("PATCH /api/keys/[id] updates permissions and rejects invalid payloads", as
|
||||
{ params: Promise.resolve({ id: "missing" }) }
|
||||
);
|
||||
|
||||
const patchBody = await patchResponse.json();
|
||||
const invalidJsonBody = await invalidJsonResponse.json();
|
||||
const missingKeyBody = await missingKeyResponse.json();
|
||||
const patchBody = (await patchResponse.json()) as any;
|
||||
const invalidJsonBody = (await invalidJsonResponse.json()) as any;
|
||||
const missingKeyBody = (await missingKeyResponse.json()) as any;
|
||||
const updated = await apiKeysDb.getApiKeyById(created.id);
|
||||
|
||||
assert.equal(patchResponse.status, 200);
|
||||
@@ -453,8 +453,8 @@ test("DELETE /api/keys/[id] removes keys and reports missing resources", async (
|
||||
{ params: Promise.resolve({ id: "missing" }) }
|
||||
);
|
||||
|
||||
const deleteBody = await deleteResponse.json();
|
||||
const missingDeleteBody = await missingDeleteResponse.json();
|
||||
const deleteBody = (await deleteResponse.json()) as any;
|
||||
const missingDeleteBody = (await missingDeleteResponse.json()) as any;
|
||||
|
||||
assert.equal(deleteResponse.status, 200);
|
||||
assert.equal(deleteBody.message, "Key deleted successfully");
|
||||
|
||||
@@ -84,7 +84,7 @@ test("critical routes: v1 management proxies covers auth, lookup, where-used, pa
|
||||
},
|
||||
})
|
||||
);
|
||||
const created = await createResponse.json();
|
||||
const created = (await createResponse.json()) as any;
|
||||
|
||||
await localDb.assignProxyToScope("provider", "openai", created.id);
|
||||
|
||||
@@ -142,17 +142,17 @@ test("critical routes: v1 management proxies covers auth, lookup, where-used, pa
|
||||
)
|
||||
);
|
||||
|
||||
const unauthenticatedBody = await unauthenticated.json();
|
||||
const invalidTokenBody = await invalidToken.json();
|
||||
const getByIdBody = await getById.json();
|
||||
const whereUsedBody = await whereUsed.json();
|
||||
const missingGetBody = await missingGet.json();
|
||||
const invalidJsonPatchBody = await invalidJsonPatch.json();
|
||||
const invalidPatchBody = await invalidPatch.json();
|
||||
const validPatchBody = await validPatch.json();
|
||||
const missingDeleteBody = await missingDelete.json();
|
||||
const conflictDeleteBody = await conflictDelete.json();
|
||||
const forcedDeleteBody = await forcedDelete.json();
|
||||
const unauthenticatedBody = (await unauthenticated.json()) as any;
|
||||
const invalidTokenBody = (await invalidToken.json()) as any;
|
||||
const getByIdBody = (await getById.json()) as any;
|
||||
const whereUsedBody = (await whereUsed.json()) as any;
|
||||
const missingGetBody = (await missingGet.json()) as any;
|
||||
const invalidJsonPatchBody = (await invalidJsonPatch.json()) as any;
|
||||
const invalidPatchBody = (await invalidPatch.json()) as any;
|
||||
const validPatchBody = (await validPatch.json()) as any;
|
||||
const missingDeleteBody = (await missingDelete.json()) as any;
|
||||
const conflictDeleteBody = (await conflictDelete.json()) as any;
|
||||
const forcedDeleteBody = (await forcedDelete.json()) as any;
|
||||
|
||||
assert.equal(unauthenticated.status, 401);
|
||||
assert.equal(unauthenticatedBody.error.message, "Authentication required");
|
||||
@@ -210,7 +210,7 @@ test("critical routes: v1 management proxies validates create payloads and clamp
|
||||
},
|
||||
})
|
||||
);
|
||||
const created = await createResponse.json();
|
||||
const created = (await createResponse.json()) as any;
|
||||
createdIds.push(created.id);
|
||||
assert.equal(createResponse.status, 201);
|
||||
}
|
||||
@@ -232,11 +232,11 @@ test("critical routes: v1 management proxies validates create payloads and clamp
|
||||
})
|
||||
);
|
||||
|
||||
const invalidJsonPostBody = await invalidJsonPost.json();
|
||||
const invalidPostBody = await invalidPost.json();
|
||||
const pagedListBody = await pagedList.json();
|
||||
const missingPatchBody = await missingPatch.json();
|
||||
const missingDeleteBody = await missingDelete.json();
|
||||
const invalidJsonPostBody = (await invalidJsonPost.json()) as any;
|
||||
const invalidPostBody = (await invalidPost.json()) as any;
|
||||
const pagedListBody = (await pagedList.json()) as any;
|
||||
const missingPatchBody = (await missingPatch.json()) as any;
|
||||
const missingDeleteBody = (await missingDelete.json()) as any;
|
||||
|
||||
assert.equal(invalidJsonPost.status, 400);
|
||||
assert.equal(invalidJsonPostBody.error.message, "Invalid JSON body");
|
||||
@@ -304,13 +304,13 @@ test("critical routes: v1 management proxies requires auth on mutating routes",
|
||||
);
|
||||
|
||||
for (const response of [unauthenticatedPost, unauthenticatedPatch, unauthenticatedDelete]) {
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(body.error.message, "Authentication required");
|
||||
}
|
||||
|
||||
for (const response of [invalidPost, invalidPatch, invalidDelete]) {
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
assert.equal(response.status, 403);
|
||||
assert.equal(body.error.message, "Invalid management token");
|
||||
}
|
||||
@@ -370,14 +370,14 @@ test("critical routes: settings proxy resolves config, validates payloads, and d
|
||||
new Request("http://localhost/api/settings/proxy")
|
||||
);
|
||||
|
||||
const invalidJsonBody = await invalidJson.json();
|
||||
const invalidProvidersBody = await invalidProviders.json();
|
||||
const setProviderProxyBody = await setProviderProxy.json();
|
||||
const getProviderProxyBody = await getProviderProxy.json();
|
||||
const resolveProxyBody = await resolveProxy.json();
|
||||
const missingLevelDeleteBody = await missingLevelDelete.json();
|
||||
const deleteProviderProxyBody = await deleteProviderProxy.json();
|
||||
const getFullConfigBody = await getFullConfig.json();
|
||||
const invalidJsonBody = (await invalidJson.json()) as any;
|
||||
const invalidProvidersBody = (await invalidProviders.json()) as any;
|
||||
const setProviderProxyBody = (await setProviderProxy.json()) as any;
|
||||
const getProviderProxyBody = (await getProviderProxy.json()) as any;
|
||||
const resolveProxyBody = (await resolveProxy.json()) as any;
|
||||
const missingLevelDeleteBody = (await missingLevelDelete.json()) as any;
|
||||
const deleteProviderProxyBody = (await deleteProviderProxy.json()) as any;
|
||||
const getFullConfigBody = (await getFullConfig.json()) as any;
|
||||
|
||||
assert.equal(invalidJson.status, 400);
|
||||
assert.equal(invalidJsonBody.error.message, "Invalid JSON body");
|
||||
@@ -413,7 +413,7 @@ test("critical routes: settings proxy prefers registry assignment for global loo
|
||||
const response = await settingsProxyRoute.GET(
|
||||
new Request("http://localhost/api/settings/proxy?level=global")
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.level, "global");
|
||||
@@ -470,10 +470,10 @@ test("critical routes: settings proxy covers global fallback and socks5 gating",
|
||||
})
|
||||
);
|
||||
|
||||
const setLegacyGlobalProxyBody = await setLegacyGlobalProxy.json();
|
||||
const getLegacyGlobalProxyBody = await getLegacyGlobalProxy.json();
|
||||
const socksDisabledBody = await socksDisabled.json();
|
||||
const socksEnabledBody = await socksEnabled.json();
|
||||
const setLegacyGlobalProxyBody = (await setLegacyGlobalProxy.json()) as any;
|
||||
const getLegacyGlobalProxyBody = (await getLegacyGlobalProxy.json()) as any;
|
||||
const socksDisabledBody = (await socksDisabled.json()) as any;
|
||||
const socksEnabledBody = (await socksEnabled.json()) as any;
|
||||
|
||||
assert.equal(setLegacyGlobalProxy.status, 200);
|
||||
assert.equal(setLegacyGlobalProxyBody.global.host, "legacy.proxy.local");
|
||||
@@ -490,7 +490,7 @@ test("critical routes: v1 models route exposes CORS and list contracts", async (
|
||||
const response = await v1ModelsRoute.GET(
|
||||
new Request("http://localhost/api/v1/models", { method: "GET" })
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(options.status, 200);
|
||||
assert.match(options.headers.get("Access-Control-Allow-Methods") || "", /GET/);
|
||||
|
||||
@@ -471,7 +471,7 @@ test("chat pipeline handles OpenAI passthrough with valid API key auth", async (
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.match(fetchCalls[0].url, /\/chat\/completions$/);
|
||||
@@ -504,7 +504,7 @@ test("chat pipeline persists Codex responses cache and reasoning tokens to call
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
const callLog = await waitFor(() => getLatestCallLog());
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
@@ -639,7 +639,7 @@ test("chat pipeline translates OpenAI requests to Claude and returns OpenAI-shap
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.match(fetchCalls[0].url, /\?beta=true$/);
|
||||
@@ -673,7 +673,7 @@ test("chat pipeline translates OpenAI requests to Gemini and returns OpenAI-shap
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.match(fetchCalls[0].url, /generateContent$/);
|
||||
@@ -710,7 +710,7 @@ test("chat pipeline translates Claude-format requests into OpenAI upstream and b
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.match(fetchCalls[0].url, /\/chat\/completions$/);
|
||||
@@ -757,7 +757,7 @@ test("chat pipeline rejects invalid API keys and malformed JSON bodies", async (
|
||||
},
|
||||
})
|
||||
);
|
||||
const invalidKeyJson = await invalidKeyResponse.json();
|
||||
const invalidKeyJson = (await invalidKeyResponse.json()) as any;
|
||||
|
||||
const invalidJsonResponse = await handleChat(
|
||||
new Request("http://localhost/v1/chat/completions", {
|
||||
@@ -766,7 +766,7 @@ test("chat pipeline rejects invalid API keys and malformed JSON bodies", async (
|
||||
body: "{bad-json",
|
||||
})
|
||||
);
|
||||
const invalidJson = await invalidJsonResponse.json();
|
||||
const invalidJson = (await invalidJsonResponse.json()) as any;
|
||||
|
||||
assert.equal(invalidKeyResponse.status, 401);
|
||||
assert.match(invalidKeyJson.error.message, /Invalid API key/i);
|
||||
@@ -786,7 +786,7 @@ test("chat pipeline rejects requests without a bearer key when strict API key mo
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.match(json.error.message, /Missing API key/i);
|
||||
@@ -801,7 +801,7 @@ test("chat pipeline returns 400 when the model field is omitted", async () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(json.error.message, /Missing model/i);
|
||||
@@ -857,7 +857,7 @@ test("chat pipeline supports local mode without Authorization on explicit combos
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(json.choices[0].message.content, "Local combo route");
|
||||
@@ -901,7 +901,7 @@ test("chat pipeline returns current no-credentials contract when no provider con
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(json.error.message, /No credentials for provider: openai/);
|
||||
});
|
||||
@@ -925,7 +925,7 @@ test("chat pipeline surfaces upstream 500 responses as structured errors", async
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 500);
|
||||
assert.match(json.error.message, /\[500\]: provider exploded/);
|
||||
});
|
||||
@@ -963,7 +963,7 @@ test("chat pipeline returns 429 with Retry-After when the upstream rate-limits t
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 429);
|
||||
assert.ok(attempts >= 1, "expected at least one upstream attempt");
|
||||
assert.ok(Number(response.headers.get("Retry-After")) >= 1);
|
||||
@@ -1029,7 +1029,7 @@ test("chat pipeline maps upstream timeouts to 504 responses", async () => {
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 504);
|
||||
assert.match(json.error.message, /\[504\]: upstream timed out/);
|
||||
});
|
||||
@@ -1068,7 +1068,7 @@ test("chat pipeline injects memory context before sending the upstream request",
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(fetchCalls[0].body.messages[0].role, "system");
|
||||
@@ -1129,7 +1129,7 @@ test("chat pipeline injects skills into tools and intercepts tool calls with ski
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.ok(Array.isArray(fetchCalls[0].body.tools));
|
||||
@@ -1176,7 +1176,7 @@ test("chat pipeline falls back to the next account after a provider failure", as
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(seenAuthHeaders, [
|
||||
"Bearer sk-openai-primary-fallback",
|
||||
@@ -1224,7 +1224,7 @@ test("chat pipeline falls back across combo models when the first provider fails
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(attempts.length, 2);
|
||||
assert.match(attempts[0].url, /\/chat\/completions$/);
|
||||
|
||||
@@ -64,7 +64,7 @@ test("combo routes requests by exact combo name", async () => {
|
||||
body: buildOpenAIChatBody("router-priority"),
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
@@ -165,7 +165,7 @@ test("priority combo falls back to the secondary model when the first one fails"
|
||||
};
|
||||
|
||||
const response = await handleChat(buildRequest({ body: buildOpenAIChatBody("router-fallback") }));
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(attempts.length, 2);
|
||||
@@ -227,7 +227,7 @@ test("priority combo can repeat the same provider/model with different fixed acc
|
||||
body: buildOpenAIChatBody("router-fixed-accounts"),
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(authHeaders.length, 2);
|
||||
@@ -297,7 +297,7 @@ test("model combo mappings route explicit model ids through the configured combo
|
||||
body: buildOpenAIChatBody("tenant/mapped-model"),
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
@@ -332,7 +332,7 @@ test("wildcard model combo mappings resolve arbitrary matching models", async ()
|
||||
body: buildOpenAIChatBody("tenant/any-model-name"),
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
@@ -347,7 +347,7 @@ test("unmapped custom model requests fail after combo resolution falls through",
|
||||
body: buildOpenAIChatBody("tenant/unmapped-model"),
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(json.error.message, /No credentials for provider: tenant/);
|
||||
@@ -378,7 +378,7 @@ test("strategy updates take effect for later requests on the same combo name", a
|
||||
body: buildOpenAIChatBody("router-dynamic", "Route dynamic initial"),
|
||||
})
|
||||
);
|
||||
await combosDb.updateCombo(combo.id, {
|
||||
await combosDb.updateCombo((combo as any).id, {
|
||||
strategy: "round-robin",
|
||||
config: { maxRetries: 0, retryDelayMs: 0 },
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ function dropFts5Artifacts() {
|
||||
"DROP TRIGGER IF EXISTS memory_fts_au;" +
|
||||
"DROP TABLE IF EXISTS memory_fts;"
|
||||
);
|
||||
} catch (_) {
|
||||
} catch (_: any) {
|
||||
/* ignore if already dropped or DB not yet initialized */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ describe("Performance: memory API route handler (1000 records)", () => {
|
||||
|
||||
assert.equal(response.status, 200, "Response should be 200 OK");
|
||||
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
assert.equal(body.data.length, 50, "Should return 50 items");
|
||||
assert.equal(body.total, MEMORY_COUNT, "Total should be 1000");
|
||||
assert.ok(body.stats, "Response should include stats");
|
||||
|
||||
@@ -50,7 +50,7 @@ test("integration: proxy registry full flow works and enforces safe delete", asy
|
||||
})
|
||||
);
|
||||
assert.equal(createRes.status, 201);
|
||||
const createdProxy = await createRes.json();
|
||||
const createdProxy = (await createRes.json()) as any;
|
||||
assert.ok(createdProxy.id);
|
||||
|
||||
const assignRes = await proxyAssignmentsRoute.PUT(
|
||||
@@ -72,7 +72,7 @@ test("integration: proxy registry full flow works and enforces safe delete", asy
|
||||
)
|
||||
);
|
||||
assert.equal(resolveRes.status, 200);
|
||||
const resolved = await resolveRes.json();
|
||||
const resolved = (await resolveRes.json()) as any;
|
||||
assert.equal(resolved.level, "account");
|
||||
assert.equal(resolved.source, "registry");
|
||||
assert.equal(resolved.proxy.host, "flow.local");
|
||||
@@ -89,7 +89,7 @@ test("integration: proxy registry full flow works and enforces safe delete", asy
|
||||
})
|
||||
);
|
||||
assert.equal(bulkRes.status, 200);
|
||||
const bulkPayload = await bulkRes.json();
|
||||
const bulkPayload = (await bulkRes.json()) as any;
|
||||
assert.equal(bulkPayload.updated, 2);
|
||||
assert.equal(bulkPayload.failed.length, 0);
|
||||
|
||||
@@ -114,7 +114,7 @@ test("integration: proxy registry full flow works and enforces safe delete", asy
|
||||
new Request("http://localhost/api/settings/proxies/health?hours=24")
|
||||
);
|
||||
assert.equal(healthRes.status, 200);
|
||||
const healthPayload = await healthRes.json();
|
||||
const healthPayload = (await healthRes.json()) as any;
|
||||
const row = healthPayload.items.find((item) => item.proxyId === createdProxy.id);
|
||||
assert.ok(row);
|
||||
assert.equal(row.totalRequests >= 2, true);
|
||||
@@ -126,7 +126,7 @@ test("integration: proxy registry full flow works and enforces safe delete", asy
|
||||
})
|
||||
);
|
||||
assert.equal(deleteConflictRes.status, 409);
|
||||
const deleteConflict = await deleteConflictRes.json();
|
||||
const deleteConflict = (await deleteConflictRes.json()) as any;
|
||||
assert.equal(deleteConflict.error.type, "conflict");
|
||||
assert.equal(typeof deleteConflict.requestId, "string");
|
||||
assert.equal(deleteConflict.requestId.length > 0, true);
|
||||
@@ -163,6 +163,6 @@ test("integration: proxy registry full flow works and enforces safe delete", asy
|
||||
})
|
||||
);
|
||||
assert.equal(deleteOkRes.status, 200);
|
||||
const deleteOkPayload = await deleteOkRes.json();
|
||||
const deleteOkPayload = (await deleteOkRes.json()) as any;
|
||||
assert.equal(deleteOkPayload.success, true);
|
||||
});
|
||||
|
||||
@@ -237,7 +237,7 @@ async function waitForServer(
|
||||
});
|
||||
if (response.ok) return;
|
||||
lastError = `HTTP ${response.status}`;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
await sleep(500);
|
||||
@@ -383,14 +383,14 @@ async function patchResilience(baseUrl: string, config: Record<string, unknown>)
|
||||
body: JSON.stringify(config),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
assert.equal(response.status, 200, JSON.stringify(payload));
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function getJson(url: string) {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(10_000) });
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
return { response, json };
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ test("skills API lists registered skills", async () => {
|
||||
});
|
||||
|
||||
const response = await skillsRouteModule.GET();
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(Array.isArray(json.skills));
|
||||
@@ -170,7 +170,7 @@ test("matching tool calls execute the registered skill and return tool results",
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(json.choices[0].finish_reason, "tool_calls");
|
||||
@@ -202,7 +202,7 @@ test("non-matching responses fall through the pipeline normally", async () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(json.choices[0].message.content, "Normal pipeline response");
|
||||
@@ -275,7 +275,7 @@ test("skill execution errors are returned gracefully in tool results", async ()
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(json.tool_results[0].tool_call_id, "call_broken");
|
||||
@@ -350,10 +350,13 @@ test("injectSkills() correctly injects skill context into a request", async () =
|
||||
|
||||
assert.ok(Array.isArray(tools), "injectSkills should return an array");
|
||||
assert.equal(tools.length, 1, "should inject exactly one skill tool");
|
||||
assert.equal(tools[0].type, "function");
|
||||
assert.equal(tools[0].function.name, "translateText@1.0.0");
|
||||
assert.equal(tools[0].function.description, "Translate text to another language");
|
||||
assert.ok(tools[0].function.parameters, "parameters should be present");
|
||||
assert.equal((tools[0] as any).type, "function");
|
||||
assert.equal((tools as any)[0].function.name, "translateText@1.0.0");
|
||||
(assert as any).equal(
|
||||
(tools[0] as any).function.description,
|
||||
"Translate text to another language"
|
||||
);
|
||||
assert.ok((tools[0] as any).function.parameters, "parameters should be present");
|
||||
});
|
||||
|
||||
test("injectSkills() merges with existing tools without duplicating", async () => {
|
||||
@@ -384,7 +387,7 @@ test("injectSkills() merges with existing tools without duplicating", async () =
|
||||
});
|
||||
|
||||
assert.equal(tools.length, 2, "should have injected skill + existing tool");
|
||||
const names = tools.map((t) => t.function?.name || t.name);
|
||||
const names = tools.map((t) => (t as any).function?.name || (t as any).name);
|
||||
assert.ok(names.includes("calcRoute@1.0.0"));
|
||||
assert.ok(names.includes("preExistingTool"));
|
||||
});
|
||||
@@ -599,7 +602,7 @@ test("skills pipeline can be disabled via skillsEnabled flag without crashing",
|
||||
}
|
||||
),
|
||||
(err) => {
|
||||
assert.ok(err.message.includes("disabled"), "should mention disabled in error");
|
||||
assert.ok((err as any).message.includes("disabled"), "should mention disabled in error");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
@@ -830,7 +833,7 @@ test("web_search fallback converts built-in tools for unsupported providers and
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(upstreamBodies.length, 1);
|
||||
@@ -896,7 +899,7 @@ test("web_search fallback preserves Responses API output by appending function_c
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
const functionCall = json.output.find((item) => item.type === "function_call");
|
||||
const functionCallOutput = json.output.find((item) => item.type === "function_call_output");
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ test("contract: /api/v1 and /api/v1/models return consistent model IDs", async (
|
||||
assert.equal(v1Response.status, 200);
|
||||
assert.equal(v1ModelsResponse.status, 200);
|
||||
|
||||
const v1Body = await v1Response.json();
|
||||
const v1ModelsBody = await v1ModelsResponse.json();
|
||||
const v1Body = (await v1Response.json()) as any;
|
||||
const v1ModelsBody = (await v1ModelsResponse.json()) as any;
|
||||
|
||||
assert.equal(v1Body.object, "list");
|
||||
assert.equal(v1ModelsBody.object, "list");
|
||||
@@ -55,7 +55,7 @@ test("contract: /api/v1/models returns OpenAI-compatible model shape", async ()
|
||||
const response = await getV1Models(new Request(`${BASE_URL}/api/v1/models`, { method: "GET" }));
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(body.object, "list");
|
||||
assert.ok(Array.isArray(body.data));
|
||||
@@ -75,7 +75,7 @@ test("contract: /api/v1/embeddings GET returns embedding model listing shape", a
|
||||
const response = await getEmbeddings();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(body.object, "list");
|
||||
assert.ok(Array.isArray(body.data));
|
||||
@@ -93,7 +93,7 @@ test("contract: /api/v1/images/generations GET returns image model listing shape
|
||||
const response = await getImageModels();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(body.object, "list");
|
||||
assert.ok(Array.isArray(body.data));
|
||||
@@ -118,7 +118,7 @@ test("contract: /api/v1/messages/count_tokens returns 400 on invalid JSON", asyn
|
||||
);
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
assert.ok(body.error, "error payload should exist");
|
||||
assert.ok(
|
||||
typeof body.error === "string" || typeof body.error === "object",
|
||||
@@ -138,7 +138,7 @@ test("contract: /api/v1/messages/count_tokens rejects empty messages payload", a
|
||||
);
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
assert.ok(body.error, "error payload should exist");
|
||||
assert.ok(
|
||||
typeof body.error === "string" || typeof body.error === "object",
|
||||
@@ -168,6 +168,6 @@ test("contract: /api/v1/messages/count_tokens computes token estimate from text
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
assert.equal(body.input_tokens, 3);
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ console.log(` Stream: ${body.stream || false}`);
|
||||
|
||||
console.log(`\n\n✅ Received ${chunkCount} chunks`);
|
||||
} else {
|
||||
const responseData = await response.json();
|
||||
const responseData = (await response.json()) as any;
|
||||
console.log("\n📦 Response:");
|
||||
console.log(JSON.stringify(responseData, null, 2));
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ test("GET /api/acp/agents requires authentication when login is enabled", async
|
||||
process.env.INITIAL_PASSWORD = "route-auth-required";
|
||||
|
||||
const response = await routeModule.GET(makeRequest("GET"));
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(body.error, "Unauthorized");
|
||||
@@ -95,7 +95,7 @@ test("POST /api/acp/agents rejects unsafe version commands for authenticated ses
|
||||
token
|
||||
)
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(body.error, /Invalid versionCommand/i);
|
||||
|
||||
@@ -139,7 +139,7 @@ test("provider create/update/delete routes emit sanitized credential audit event
|
||||
);
|
||||
|
||||
assert.equal(createResponse.status, 201);
|
||||
const createBody = await createResponse.json();
|
||||
const createBody = (await createResponse.json()) as any;
|
||||
const connectionId = createBody.connection.id;
|
||||
assert.equal(typeof connectionId, "string");
|
||||
|
||||
@@ -179,23 +179,23 @@ test("provider create/update/delete routes emit sanitized credential audit event
|
||||
assert.equal(createdEvent.resourceType, "provider_credentials");
|
||||
assert.equal(createdEvent.requestId, "req-provider-create");
|
||||
assert.equal(createdEvent.target, "openai:Primary OpenAI");
|
||||
assert.equal("apiKey" in createdEvent.metadata.connection, false);
|
||||
assert.equal("apiKey" in (createdEvent.metadata as any).connection, false);
|
||||
|
||||
const updatedEvent = compliance.getAuditLog({ action: "provider.credentials.updated" })[0];
|
||||
assert.equal(updatedEvent.requestId, "req-provider-update");
|
||||
assert.deepEqual(updatedEvent.metadata.changedFields.sort(), [
|
||||
assert.deepEqual((updatedEvent as any).metadata.changedFields.sort(), [
|
||||
"defaultModel",
|
||||
"isActive",
|
||||
"name",
|
||||
]);
|
||||
assert.equal(updatedEvent.metadata.before.name, "Primary OpenAI");
|
||||
assert.equal(updatedEvent.metadata.after.name, "Primary OpenAI Updated");
|
||||
assert.equal("apiKey" in updatedEvent.metadata.before, false);
|
||||
assert.equal("apiKey" in updatedEvent.metadata.after, false);
|
||||
assert.equal((updatedEvent as any).metadata.before.name, "Primary OpenAI");
|
||||
(assert as any).equal((updatedEvent.metadata as any).after.name, "Primary OpenAI Updated");
|
||||
(assert as any).equal("apiKey" in (updatedEvent.metadata as any).before, false);
|
||||
(assert as any).equal("apiKey" in (updatedEvent.metadata as any).after, false);
|
||||
|
||||
const revokedEvent = compliance.getAuditLog({ action: "provider.credentials.revoked" })[0];
|
||||
assert.equal(revokedEvent.requestId, "req-provider-delete");
|
||||
assert.equal(revokedEvent.target, "openai:Primary OpenAI Updated");
|
||||
assert.equal(revokedEvent.status, "success");
|
||||
assert.equal("apiKey" in revokedEvent.metadata.connection, false);
|
||||
assert.equal("apiKey" in (revokedEvent.metadata as any).connection, false);
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ async function resetStorage() {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
}
|
||||
break;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if ((error?.code === "EBUSY" || error?.code === "EPERM") && attempt < 9) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
||||
} else {
|
||||
@@ -73,7 +73,7 @@ function makePolicyRequest(apiKey) {
|
||||
}
|
||||
|
||||
async function readErrorMessage(response) {
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
return body.error.message;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ test("GET /api/keys stays masked even when reveal is enabled", async () => {
|
||||
const created = await apiKeysDb.createApiKey("Primary Key", MACHINE_ID);
|
||||
|
||||
const response = await listRoute.GET(new Request("http://localhost/api/keys"));
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.allowKeyReveal, true);
|
||||
@@ -59,7 +59,7 @@ test("GET /api/keys falls back to default pagination for invalid query params",
|
||||
const response = await listRoute.GET(
|
||||
new Request("http://localhost/api/keys?limit=abc&offset=xyz")
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.allowKeyReveal, false);
|
||||
@@ -92,7 +92,7 @@ test("GET /api/keys returns 500 when key loading fails unexpectedly", async () =
|
||||
|
||||
try {
|
||||
const response = await listRoute.GET(new Request("http://localhost/api/keys"));
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 500);
|
||||
assert.equal(body.error, "Failed to fetch keys");
|
||||
@@ -114,7 +114,7 @@ test("GET /api/keys/[id]/reveal rejects requests when reveal is disabled", async
|
||||
const response = await revealRoute.GET(request, {
|
||||
params: Promise.resolve({ id: created.id }),
|
||||
});
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 403);
|
||||
assert.equal(body.error, "API key reveal is disabled");
|
||||
@@ -128,7 +128,7 @@ test("GET /api/keys/[id]/reveal returns the full key when reveal is enabled", as
|
||||
const response = await revealRoute.GET(request, {
|
||||
params: Promise.resolve({ id: created.id }),
|
||||
});
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.key, created.key);
|
||||
@@ -141,7 +141,7 @@ test("GET /api/keys/[id]/reveal returns 404 for unknown keys even when reveal is
|
||||
const response = await revealRoute.GET(request, {
|
||||
params: Promise.resolve({ id: "missing" }),
|
||||
});
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 404);
|
||||
assert.equal(body.error, "Key not found");
|
||||
@@ -154,7 +154,7 @@ test("GET /api/keys/[id]/reveal returns 500 when params resolution fails", async
|
||||
const response = await revealRoute.GET(request, {
|
||||
params: Promise.reject(new Error("params exploded")),
|
||||
});
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 500);
|
||||
assert.equal(body.error, "Failed to reveal key");
|
||||
|
||||
@@ -8,7 +8,7 @@ test("handleAudioSpeech requires model", async () => {
|
||||
body: { input: "hello" },
|
||||
credentials: { apiKey: "x" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(payload.error.message, "model is required");
|
||||
@@ -19,7 +19,7 @@ test("handleAudioSpeech requires input text", async () => {
|
||||
body: { model: "openai/tts-1" },
|
||||
credentials: { apiKey: "x" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(payload.error.message, "input is required");
|
||||
@@ -121,7 +121,7 @@ test("handleAudioSpeech rejects invalid ElevenLabs voice identifiers", async ()
|
||||
},
|
||||
credentials: { apiKey: "xi-key" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(payload.error.message, "Invalid voice ID");
|
||||
@@ -216,7 +216,7 @@ test("handleAudioSpeech requires credentials for authenticated providers", async
|
||||
},
|
||||
credentials: null,
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(payload.error.message, "No credentials for speech provider: openai");
|
||||
@@ -299,7 +299,7 @@ test("handleAudioSpeech validates HuggingFace model identifiers", async () => {
|
||||
},
|
||||
credentials: { apiKey: "hf-key" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(payload.error.message, "Invalid model ID");
|
||||
@@ -455,7 +455,7 @@ test("handleAudioSpeech surfaces parsed upstream error messages", async () => {
|
||||
},
|
||||
credentials: { apiKey: "openai-key" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 429);
|
||||
assert.equal(payload.error.message, "quota exceeded");
|
||||
@@ -479,7 +479,7 @@ test("handleAudioSpeech returns a 500 when the provider request throws", async (
|
||||
},
|
||||
credentials: { apiKey: "openai-key" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 500);
|
||||
assert.equal(payload.error.message, "Speech request failed: socket hang up");
|
||||
|
||||
@@ -17,7 +17,7 @@ test("handleAudioTranscription requires model", async () => {
|
||||
formData.append("file", buildFile("abc", "audio.wav", "audio/wav"));
|
||||
|
||||
const response = await handleAudioTranscription({ formData, credentials: { apiKey: "x" } });
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(payload.error.message, "model is required");
|
||||
@@ -28,7 +28,7 @@ test("handleAudioTranscription requires a file upload", async () => {
|
||||
formData.append("model", "openai/whisper-1");
|
||||
|
||||
const response = await handleAudioTranscription({ formData, credentials: { apiKey: "x" } });
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(payload.error.message, "file is required");
|
||||
@@ -119,7 +119,7 @@ test("handleAudioTranscription routes Deepgram with binary upload and language p
|
||||
formData,
|
||||
credentials: { apiKey: "dg-key" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
const url = new URL(capturedUrl);
|
||||
assert.equal(url.origin + url.pathname, "https://api.deepgram.com/v1/listen");
|
||||
@@ -213,7 +213,7 @@ test("handleAudioTranscription rejects invalid HuggingFace model paths", async (
|
||||
formData,
|
||||
credentials: { apiKey: "hf-key" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(payload.error.message, "Invalid model ID");
|
||||
@@ -225,7 +225,7 @@ test("handleAudioTranscription requires credentials for authenticated providers"
|
||||
formData.append("file", buildFile("abc", "clip.wav", "audio/wav"));
|
||||
|
||||
const response = await handleAudioTranscription({ formData, credentials: null });
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(payload.error.message, "No credentials for transcription provider: openai");
|
||||
@@ -338,7 +338,7 @@ test("handleAudioTranscription returns an error when AssemblyAI reports a termin
|
||||
formData,
|
||||
credentials: { apiKey: "assembly-key" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 500);
|
||||
assert.equal(payload.error.message, "corrupt audio payload");
|
||||
@@ -397,7 +397,7 @@ test("handleAudioTranscription rejects unsupported providers", async () => {
|
||||
formData,
|
||||
credentials: { apiKey: "x" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(
|
||||
@@ -424,7 +424,7 @@ test("handleAudioTranscription surfaces parsed upstream errors for OpenAI-compat
|
||||
formData,
|
||||
credentials: { apiKey: "openai-key" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 429);
|
||||
assert.equal(payload.error.message, "too many requests");
|
||||
@@ -449,7 +449,7 @@ test("handleAudioTranscription returns a 500 when upstream fetch throws", async
|
||||
formData,
|
||||
credentials: { apiKey: "openai-key" },
|
||||
});
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 500);
|
||||
assert.equal(payload.error.message, "Transcription request failed: network timeout");
|
||||
|
||||
@@ -45,9 +45,9 @@ test("clearAccountError clears stale provider error metadata after recovery", as
|
||||
assert.equal(credentials.errorCode, "refresh_failed");
|
||||
assert.equal(credentials.lastErrorType, "token_refresh_failed");
|
||||
assert.equal(credentials.lastErrorSource, "oauth");
|
||||
await auth.clearAccountError(created.id, credentials);
|
||||
await auth.clearAccountError((created as any).id, credentials);
|
||||
|
||||
const updated = await providersDb.getProviderConnectionById(created.id);
|
||||
const updated = await providersDb.getProviderConnectionById((created as any).id);
|
||||
assert.equal(updated.testStatus, "active");
|
||||
assert.equal(updated.lastError, undefined);
|
||||
assert.equal(updated.lastErrorType, undefined);
|
||||
@@ -68,7 +68,7 @@ test("clearAccountError is a no-op when the connection is already clean", async
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
await auth.clearAccountError(created.id, {
|
||||
await auth.clearAccountError((created as any).id, {
|
||||
connectionId: created.id,
|
||||
testStatus: "active",
|
||||
lastError: null,
|
||||
@@ -78,7 +78,7 @@ test("clearAccountError is a no-op when the connection is already clean", async
|
||||
lastErrorSource: null,
|
||||
});
|
||||
|
||||
const updated = await providersDb.getProviderConnectionById(created.id);
|
||||
const updated = await providersDb.getProviderConnectionById((created as any).id);
|
||||
assert.equal(updated.testStatus, "active");
|
||||
assert.equal(updated.backoffLevel, 0);
|
||||
assert.equal(updated.lastError, undefined);
|
||||
@@ -114,7 +114,7 @@ test("clearRecoveredProviderState ignores empty payloads and clears recoverable
|
||||
rateLimitedUntil: new Date(Date.now() + 60_000).toISOString(),
|
||||
});
|
||||
|
||||
const updated = await providersDb.getProviderConnectionById(created.id);
|
||||
const updated = await providersDb.getProviderConnectionById((created as any).id);
|
||||
assert.equal(updated.testStatus, "active");
|
||||
assert.equal(updated.lastError, undefined);
|
||||
assert.equal(updated.lastErrorType, undefined);
|
||||
|
||||
@@ -82,7 +82,10 @@ test("auth login route lazily migrates INITIAL_PASSWORD to a persisted hash befo
|
||||
assert.equal(setCalls.length, 1);
|
||||
assert.equal(managementPassword.isBcryptHash(settings.password), true);
|
||||
assert.equal(
|
||||
await managementPassword.verifyManagementPassword("bootstrap-secret", settings.password),
|
||||
await managementPassword.verifyManagementPassword(
|
||||
"bootstrap-secret",
|
||||
(settings as any).password
|
||||
),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
@@ -96,7 +96,7 @@ test("markAccountUnavailable does not overwrite terminal status", async () => {
|
||||
});
|
||||
|
||||
const result = await auth.markAccountUnavailable(
|
||||
conn.id,
|
||||
(conn as any).id,
|
||||
503,
|
||||
"temporary upstream error",
|
||||
"openai",
|
||||
@@ -106,7 +106,7 @@ test("markAccountUnavailable does not overwrite terminal status", async () => {
|
||||
assert.equal(result.shouldFallback, true);
|
||||
assert.equal(result.cooldownMs, 0);
|
||||
|
||||
const after = await providersDb.getProviderConnectionById(conn.id);
|
||||
const after = await providersDb.getProviderConnectionById((conn as any).id);
|
||||
assert.equal(after.testStatus, "credits_exhausted");
|
||||
});
|
||||
|
||||
@@ -122,13 +122,13 @@ test("markAccountUnavailable marks 401 connections as expired without adding coo
|
||||
});
|
||||
|
||||
const result = await auth.markAccountUnavailable(
|
||||
conn.id,
|
||||
(conn as any).id,
|
||||
401,
|
||||
"unauthorized",
|
||||
"openai",
|
||||
"gpt-4.1"
|
||||
);
|
||||
const after = await providersDb.getProviderConnectionById(conn.id);
|
||||
const after = await providersDb.getProviderConnectionById((conn as any).id);
|
||||
|
||||
assert.equal(result.shouldFallback, true);
|
||||
assert.equal(result.cooldownMs, 0);
|
||||
@@ -148,13 +148,13 @@ test("markAccountUnavailable marks 402 connections as credits_exhausted without
|
||||
});
|
||||
|
||||
const result = await auth.markAccountUnavailable(
|
||||
conn.id,
|
||||
(conn as any).id,
|
||||
402,
|
||||
"payment required",
|
||||
"openai",
|
||||
"gpt-4.1"
|
||||
);
|
||||
const after = await providersDb.getProviderConnectionById(conn.id);
|
||||
const after = await providersDb.getProviderConnectionById((conn as any).id);
|
||||
|
||||
assert.equal(result.shouldFallback, true);
|
||||
assert.equal(result.cooldownMs, 0);
|
||||
@@ -173,8 +173,14 @@ test("markAccountUnavailable marks 403 connections as banned without adding cool
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
const result = await auth.markAccountUnavailable(conn.id, 403, "forbidden", "openai", "gpt-4.1");
|
||||
const after = await providersDb.getProviderConnectionById(conn.id);
|
||||
const result = await auth.markAccountUnavailable(
|
||||
(conn as any).id,
|
||||
403,
|
||||
"forbidden",
|
||||
"openai",
|
||||
"gpt-4.1"
|
||||
);
|
||||
const after = await providersDb.getProviderConnectionById((conn as any).id);
|
||||
|
||||
assert.equal(result.shouldFallback, true);
|
||||
assert.equal(result.cooldownMs, 0);
|
||||
@@ -194,13 +200,13 @@ test("markAccountUnavailable keeps project-route 403 errors non-terminal", async
|
||||
});
|
||||
|
||||
const result = await auth.markAccountUnavailable(
|
||||
conn.id,
|
||||
(conn as any).id,
|
||||
403,
|
||||
"The service has not been used in project",
|
||||
"openai",
|
||||
"gpt-4.1"
|
||||
);
|
||||
const after = await providersDb.getProviderConnectionById(conn.id);
|
||||
const after = await providersDb.getProviderConnectionById((conn as any).id);
|
||||
|
||||
assert.equal(result.shouldFallback, true);
|
||||
assert.equal(result.cooldownMs, 0);
|
||||
@@ -221,13 +227,13 @@ test("markAccountUnavailable keeps oauth-invalid 401 errors non-terminal", async
|
||||
});
|
||||
|
||||
const result = await auth.markAccountUnavailable(
|
||||
conn.id,
|
||||
(conn as any).id,
|
||||
401,
|
||||
"Invalid authentication credentials provided",
|
||||
"openai",
|
||||
"gpt-4.1"
|
||||
);
|
||||
const after = await providersDb.getProviderConnectionById(conn.id);
|
||||
const after = await providersDb.getProviderConnectionById((conn as any).id);
|
||||
|
||||
assert.equal(result.shouldFallback, true);
|
||||
assert.equal(result.cooldownMs, 0);
|
||||
|
||||
@@ -22,8 +22,10 @@ const {
|
||||
getTerminalBatches,
|
||||
} = await import("../../src/lib/localDb.ts");
|
||||
const { getDbInstance } = await import("../../src/lib/db/core.ts");
|
||||
const { initBatchProcessor, stopBatchProcessor } =
|
||||
const { initBatchProcessor, stopBatchProcessor, processPendingBatches } =
|
||||
await import("../../open-sse/services/batchProcessor.ts");
|
||||
const batchesRoute = await import("../../src/app/api/v1/batches/route.ts");
|
||||
const filesRoute = await import("../../src/app/api/v1/files/route.ts");
|
||||
|
||||
test("Batch API and Processing", async () => {
|
||||
// 0. Setup environment, mock provider and API key
|
||||
@@ -255,6 +257,128 @@ test("Batch handles and counts failures correctly", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("Batch dispatches non-chat endpoints through the matching route handler", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
object: "list",
|
||||
data: [{ object: "embedding", embedding: [0.1, 0.2], index: 0 }],
|
||||
usage: { prompt_tokens: 2, total_tokens: 2 },
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
|
||||
initBatchProcessor();
|
||||
try {
|
||||
await createProviderConnection({
|
||||
provider: "openai",
|
||||
authType: "apikey",
|
||||
name: "Mock OpenAI Embeddings",
|
||||
apiKey: "sk-mock-embeddings-key",
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const batchItems = [
|
||||
JSON.stringify({
|
||||
custom_id: "embed-request",
|
||||
method: "POST",
|
||||
url: "/v1/embeddings",
|
||||
body: {
|
||||
model: "openai/text-embedding-3-small",
|
||||
input: "Hello embeddings",
|
||||
},
|
||||
}),
|
||||
].join("\n");
|
||||
|
||||
const file = createFile({
|
||||
bytes: Buffer.byteLength(batchItems),
|
||||
filename: "embeddings_batch.jsonl",
|
||||
purpose: "batch",
|
||||
content: Buffer.from(batchItems),
|
||||
apiKeyId: null,
|
||||
});
|
||||
|
||||
const batch = createBatch({
|
||||
endpoint: "/v1/embeddings",
|
||||
completionWindow: "24h",
|
||||
inputFileId: file.id,
|
||||
apiKeyId: null,
|
||||
});
|
||||
|
||||
let maxAttempts = 20;
|
||||
let currentBatch = getBatch(batch.id);
|
||||
while (
|
||||
maxAttempts > 0 &&
|
||||
currentBatch?.status !== "completed" &&
|
||||
currentBatch?.status !== "failed"
|
||||
) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
currentBatch = getBatch(batch.id);
|
||||
maxAttempts--;
|
||||
}
|
||||
|
||||
assert.strictEqual(currentBatch?.status, "completed", "embedding batch should complete");
|
||||
assert.strictEqual(currentBatch?.requestCountsCompleted, 1);
|
||||
assert.ok(currentBatch?.outputFileId, "embedding batch should produce an output file");
|
||||
|
||||
const outputContent = getFileContent(currentBatch.outputFileId!);
|
||||
const result = JSON.parse(outputContent.toString());
|
||||
assert.strictEqual(result.response.status_code, 200);
|
||||
assert.ok(Array.isArray(result.response.body.data));
|
||||
assert.strictEqual(result.response.body.object, "list");
|
||||
} finally {
|
||||
stopBatchProcessor();
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("Batch rejects input lines whose url does not match the batch endpoint", async () => {
|
||||
initBatchProcessor();
|
||||
try {
|
||||
const batchItems = [
|
||||
JSON.stringify({
|
||||
custom_id: "wrong-endpoint",
|
||||
method: "POST",
|
||||
url: "/v1/embeddings",
|
||||
body: {
|
||||
model: "openai/text-embedding-3-small",
|
||||
input: "Wrong endpoint",
|
||||
},
|
||||
}),
|
||||
].join("\n");
|
||||
|
||||
const file = createFile({
|
||||
bytes: Buffer.byteLength(batchItems),
|
||||
filename: "wrong_endpoint_batch.jsonl",
|
||||
purpose: "batch",
|
||||
content: Buffer.from(batchItems),
|
||||
apiKeyId: null,
|
||||
});
|
||||
|
||||
const batch = createBatch({
|
||||
endpoint: "/v1/chat/completions",
|
||||
completionWindow: "24h",
|
||||
inputFileId: file.id,
|
||||
apiKeyId: null,
|
||||
});
|
||||
|
||||
await processPendingBatches();
|
||||
|
||||
const currentBatch = getBatch(batch.id);
|
||||
assert.strictEqual(currentBatch?.status, "failed");
|
||||
assert.match(
|
||||
String(currentBatch?.errors?.[0]?.message || ""),
|
||||
/does not match batch endpoint/i
|
||||
);
|
||||
} finally {
|
||||
stopBatchProcessor();
|
||||
}
|
||||
});
|
||||
|
||||
test("Batch forces stream: false for all requests", async () => {
|
||||
initBatchProcessor();
|
||||
try {
|
||||
@@ -424,7 +548,8 @@ test("List batches pagination and response format", async () => {
|
||||
apiKeyId: apiKey.id,
|
||||
});
|
||||
|
||||
const batchIds: string[] = [];
|
||||
const batchOrder: Array<{ createdAt: number; id: string }> = [];
|
||||
const baseCreatedAt = Math.floor(Date.now() / 1000) - 10_000;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const b = createBatch({
|
||||
endpoint: "/v1/chat/completions",
|
||||
@@ -433,11 +558,12 @@ test("List batches pagination and response format", async () => {
|
||||
apiKeyId: apiKey.id,
|
||||
metadata: { index: i },
|
||||
});
|
||||
batchIds.push(b.id);
|
||||
updateBatch(b.id, { createdAt: baseCreatedAt + i });
|
||||
batchOrder.push({ createdAt: baseCreatedAt + i, id: b.id });
|
||||
}
|
||||
|
||||
// Sort batchIds in descending order as listBatches returns them by ID DESC
|
||||
batchIds.sort().reverse();
|
||||
batchOrder.sort((a, b) => b.createdAt - a.createdAt || b.id.localeCompare(a.id));
|
||||
const batchIds = batchOrder.map((entry) => entry.id);
|
||||
|
||||
// 2. Test listBatches logic (direct DB call)
|
||||
const { listBatches } = await import("../../src/lib/localDb");
|
||||
@@ -476,6 +602,92 @@ test("List batches pagination and response format", async () => {
|
||||
assert.strictEqual(data3[0].id, batchIds[4]);
|
||||
});
|
||||
|
||||
test("Batch cleanup honors output_expires_after for output artifacts", async () => {
|
||||
const apiKey = await createApiKey("Batch Retention Key", "test-machine");
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const inputFile = createFile({
|
||||
bytes: 10,
|
||||
filename: "retention_input.jsonl",
|
||||
purpose: "batch",
|
||||
content: Buffer.from("{}"),
|
||||
apiKeyId: apiKey.id,
|
||||
});
|
||||
const outputFile = createFile({
|
||||
bytes: 10,
|
||||
filename: "retention_output.jsonl",
|
||||
purpose: "batch_output",
|
||||
content: Buffer.from("{}"),
|
||||
apiKeyId: apiKey.id,
|
||||
status: "completed",
|
||||
});
|
||||
const errorFile = createFile({
|
||||
bytes: 10,
|
||||
filename: "retention_error.jsonl",
|
||||
purpose: "batch_output",
|
||||
content: Buffer.from("{}"),
|
||||
apiKeyId: apiKey.id,
|
||||
status: "completed",
|
||||
});
|
||||
|
||||
const batch = createBatch({
|
||||
endpoint: "/v1/chat/completions",
|
||||
completionWindow: "24h",
|
||||
inputFileId: inputFile.id,
|
||||
apiKeyId: apiKey.id,
|
||||
outputExpiresAfterSeconds: 3600,
|
||||
outputExpiresAfterAnchor: "created_at",
|
||||
});
|
||||
|
||||
updateBatch(batch.id, {
|
||||
status: "completed",
|
||||
createdAt: now - 3700,
|
||||
completedAt: now - 30,
|
||||
outputFileId: outputFile.id,
|
||||
errorFileId: errorFile.id,
|
||||
});
|
||||
|
||||
await processPendingBatches();
|
||||
|
||||
assert.ok(getFile(inputFile.id), "input file should still follow completion_window retention");
|
||||
assert.equal(getFile(outputFile.id), null);
|
||||
assert.equal(getFile(errorFile.id), null);
|
||||
});
|
||||
|
||||
test("Batch list route rejects missing API key when REQUIRE_API_KEY is enabled", async () => {
|
||||
const previous = process.env.REQUIRE_API_KEY;
|
||||
process.env.REQUIRE_API_KEY = "true";
|
||||
|
||||
try {
|
||||
const response = await batchesRoute.GET(new Request("http://localhost/api/v1/batches"));
|
||||
const json = await response.json();
|
||||
|
||||
assert.strictEqual(response.status, 401);
|
||||
assert.strictEqual(json.error.message, "Missing API key");
|
||||
} finally {
|
||||
process.env.REQUIRE_API_KEY = previous ?? "false";
|
||||
}
|
||||
});
|
||||
|
||||
test("Files list route rejects invalid API key when REQUIRE_API_KEY is enabled", async () => {
|
||||
const previous = process.env.REQUIRE_API_KEY;
|
||||
process.env.REQUIRE_API_KEY = "true";
|
||||
|
||||
try {
|
||||
const response = await filesRoute.GET(
|
||||
new Request("http://localhost/api/v1/files", {
|
||||
headers: { Authorization: "Bearer invalid-test-key" },
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
|
||||
assert.strictEqual(response.status, 401);
|
||||
assert.strictEqual(json.error.message, "Invalid API key");
|
||||
} finally {
|
||||
process.env.REQUIRE_API_KEY = previous ?? "false";
|
||||
}
|
||||
});
|
||||
|
||||
test("Batch Cancel API", async () => {
|
||||
const apiKey = await createApiKey("Cancel Test Key", "test-machine");
|
||||
|
||||
@@ -704,62 +916,66 @@ test("Retrieve file content spec compliance", async () => {
|
||||
});
|
||||
|
||||
test("Batch dispatches to embeddings handler for /v1/embeddings URL", async () => {
|
||||
initBatchProcessor();
|
||||
try {
|
||||
const batchItems = [
|
||||
JSON.stringify({
|
||||
custom_id: "embed-request-1",
|
||||
method: "POST",
|
||||
url: "/v1/embeddings",
|
||||
body: { model: "mistral/mistral-embed", input: "The food was delicious." }
|
||||
})
|
||||
].join("\n");
|
||||
initBatchProcessor();
|
||||
try {
|
||||
const batchItems = [
|
||||
JSON.stringify({
|
||||
custom_id: "embed-request-1",
|
||||
method: "POST",
|
||||
url: "/v1/embeddings",
|
||||
body: { model: "mistral/mistral-embed", input: "The food was delicious." },
|
||||
}),
|
||||
].join("\n");
|
||||
|
||||
const file = createFile({
|
||||
bytes: Buffer.byteLength(batchItems),
|
||||
filename: "embed_batch.jsonl",
|
||||
purpose: "batch",
|
||||
content: Buffer.from(batchItems),
|
||||
apiKeyId: null
|
||||
});
|
||||
const file = createFile({
|
||||
bytes: Buffer.byteLength(batchItems),
|
||||
filename: "embed_batch.jsonl",
|
||||
purpose: "batch",
|
||||
content: Buffer.from(batchItems),
|
||||
apiKeyId: null,
|
||||
});
|
||||
|
||||
const batch = createBatch({
|
||||
endpoint: "/v1/embeddings",
|
||||
completionWindow: "24h",
|
||||
inputFileId: file.id,
|
||||
apiKeyId: null
|
||||
});
|
||||
const batch = createBatch({
|
||||
endpoint: "/v1/embeddings",
|
||||
completionWindow: "24h",
|
||||
inputFileId: file.id,
|
||||
apiKeyId: null,
|
||||
});
|
||||
|
||||
let maxAttempts = 20;
|
||||
let currentBatch = getBatch(batch.id);
|
||||
while (maxAttempts > 0 && currentBatch?.status !== "completed" && currentBatch?.status !== "failed") {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
currentBatch = getBatch(batch.id);
|
||||
maxAttempts--;
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
currentBatch?.status === "completed" || currentBatch?.status === "failed",
|
||||
"Batch should reach a terminal state"
|
||||
);
|
||||
assert.strictEqual(currentBatch?.requestCountsTotal, 1);
|
||||
|
||||
// Verify the batch item was dispatched to the embeddings handler, not the chat handler.
|
||||
// The chat handler would return errors about missing "messages", "Missing model", etc.
|
||||
// The embeddings handler returns errors about missing credentials or invalid embedding models.
|
||||
const outputFileId = currentBatch?.outputFileId || currentBatch?.errorFileId;
|
||||
assert.ok(outputFileId, "Should have an output or error file");
|
||||
const outputContent = getFileContent(outputFileId!);
|
||||
assert.ok(outputContent, "Output file should have content");
|
||||
const result = JSON.parse(outputContent.toString());
|
||||
const errorMsg = result.response?.body?.error?.message || "";
|
||||
assert.ok(
|
||||
!errorMsg.includes("messages") && !errorMsg.includes("Missing model"),
|
||||
`Error should not be a chat-specific error. Got: ${errorMsg}`
|
||||
);
|
||||
} finally {
|
||||
stopBatchProcessor();
|
||||
let maxAttempts = 20;
|
||||
let currentBatch = getBatch(batch.id);
|
||||
while (
|
||||
maxAttempts > 0 &&
|
||||
currentBatch?.status !== "completed" &&
|
||||
currentBatch?.status !== "failed"
|
||||
) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
currentBatch = getBatch(batch.id);
|
||||
maxAttempts--;
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
currentBatch?.status === "completed" || currentBatch?.status === "failed",
|
||||
"Batch should reach a terminal state"
|
||||
);
|
||||
assert.strictEqual(currentBatch?.requestCountsTotal, 1);
|
||||
|
||||
// Verify the batch item was dispatched to the embeddings handler, not the chat handler.
|
||||
// The chat handler would return errors about missing "messages", "Missing model", etc.
|
||||
// The embeddings handler returns errors about missing credentials or invalid embedding models.
|
||||
const outputFileId = currentBatch?.outputFileId || currentBatch?.errorFileId;
|
||||
assert.ok(outputFileId, "Should have an output or error file");
|
||||
const outputContent = getFileContent(outputFileId!);
|
||||
assert.ok(outputContent, "Output file should have content");
|
||||
const result = JSON.parse(outputContent.toString());
|
||||
const errorMsg = result.response?.body?.error?.message || "";
|
||||
assert.ok(
|
||||
!errorMsg.includes("messages") && !errorMsg.includes("Missing model"),
|
||||
`Error should not be a chat-specific error. Got: ${errorMsg}`
|
||||
);
|
||||
} finally {
|
||||
stopBatchProcessor();
|
||||
}
|
||||
});
|
||||
|
||||
test("getTerminalBatches returns only terminal statuses ordered oldest first", async () => {
|
||||
|
||||
@@ -85,7 +85,7 @@ test("movePath rethrows non-EXDEV rename failures", async () => {
|
||||
throw new Error("remove fallback should not run");
|
||||
},
|
||||
}),
|
||||
(error) => error?.code === "EACCES"
|
||||
(error) => (error as any).code === "EACCES"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ test("handleBypassRequest returns a canned JSON response for warmup bypasses", a
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.response.headers.get("content-type"), "application/json");
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(payload.model, "gpt-5-mini");
|
||||
assert.equal(payload.choices[0].message.role, "assistant");
|
||||
assert.match(payload.choices[0].message.content, /clear terminal/i);
|
||||
@@ -79,7 +79,7 @@ test("handleBypassRequest bypasses single-message count probes", async () => {
|
||||
);
|
||||
|
||||
assert.ok(result);
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(payload.usage.total_tokens, 2);
|
||||
assert.equal(payload.choices[0].finish_reason, "stop");
|
||||
});
|
||||
|
||||
@@ -117,9 +117,9 @@ test("saveCallLog stores only summary metadata in SQLite and writes detailed art
|
||||
assert.equal(detail?.comboStepId, "step-openai-a");
|
||||
assert.equal(detail?.comboExecutionKey, "combo-a:0:step-openai-a");
|
||||
assert.equal(detail?.pipelinePayloads?.clientRawRequest?.body?.raw, true);
|
||||
assert.equal(detail?.pipelinePayloads?.providerRequest?.body?.translated, true);
|
||||
assert.equal(detail?.pipelinePayloads?.providerResponse?.body?.upstream, true);
|
||||
assert.equal(detail?.pipelinePayloads?.clientResponse?.body?.final, true);
|
||||
assert.equal((detail?.pipelinePayloads?.providerRequest as any).body?.translated, true);
|
||||
assert.equal((detail?.pipelinePayloads as any).providerResponse?.body?.upstream, true);
|
||||
assert.equal((detail?.pipelinePayloads as any).clientResponse?.body?.final, true);
|
||||
assert.match(
|
||||
detail?.artifactRelPath || "",
|
||||
/^2026-03-30\/2026-03-30T12-34-56\.789Z_req_artifact_1\.json$/
|
||||
@@ -129,7 +129,7 @@ test("saveCallLog stores only summary metadata in SQLite and writes detailed art
|
||||
const columns = db
|
||||
.prepare("SELECT name FROM pragma_table_info('call_logs') ORDER BY cid")
|
||||
.all()
|
||||
.map((row) => row.name);
|
||||
.map((row) => (row as any).name);
|
||||
assert.equal(columns.includes("request_body"), false);
|
||||
assert.equal(columns.includes("response_body"), false);
|
||||
assert.equal(columns.includes("error"), false);
|
||||
@@ -142,12 +142,12 @@ test("saveCallLog stores only summary metadata in SQLite and writes detailed art
|
||||
`
|
||||
)
|
||||
.get(logId);
|
||||
assert.equal(summaryRow.detail_state, "ready");
|
||||
assert.equal(summaryRow.cache_source, "semantic");
|
||||
assert.equal(summaryRow.has_request_body, 1);
|
||||
assert.equal(summaryRow.has_response_body, 1);
|
||||
assert.equal(summaryRow.has_pipeline_details, 1);
|
||||
assert.equal(typeof summaryRow.artifact_relpath, "string");
|
||||
(assert as any).equal((summaryRow as any).detail_state, "ready");
|
||||
assert.equal((summaryRow as any).cache_source, "semantic");
|
||||
assert.equal((summaryRow as any).has_request_body, 1);
|
||||
assert.equal((summaryRow as any).has_response_body, 1);
|
||||
assert.equal((summaryRow as any).has_pipeline_details, 1);
|
||||
assert.equal(typeof (summaryRow as any).artifact_relpath, "string");
|
||||
|
||||
const artifactPath = path.join(TEST_DATA_DIR, "call_logs", detail.artifactRelPath);
|
||||
const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8"));
|
||||
@@ -192,12 +192,14 @@ test("rotateCallLogs removes expired rows and orphaned artifacts but keeps fresh
|
||||
.getDbInstance()
|
||||
.prepare("SELECT artifact_relpath FROM call_logs WHERE id = ?")
|
||||
.get("fresh-log");
|
||||
const freshAbsPath = path.join(TEST_DATA_DIR, "call_logs", freshRow.artifact_relpath);
|
||||
const freshAbsPath = path.join(TEST_DATA_DIR, "call_logs", (freshRow as any).artifact_relpath);
|
||||
assert.equal(
|
||||
core
|
||||
.getDbInstance()
|
||||
.prepare("SELECT COUNT(*) AS cnt FROM call_logs WHERE id = ?")
|
||||
.get("expired-log").cnt,
|
||||
(
|
||||
core
|
||||
.getDbInstance()
|
||||
.prepare("SELECT COUNT(*) AS cnt FROM call_logs WHERE id = ?")
|
||||
.get("expired-log") as any
|
||||
).cnt,
|
||||
0
|
||||
);
|
||||
assert.equal(fs.existsSync(oldAbsPath), false);
|
||||
@@ -207,7 +209,7 @@ test("rotateCallLogs removes expired rows and orphaned artifacts but keeps fresh
|
||||
|
||||
const db = core.getDbInstance();
|
||||
assert.equal(
|
||||
db.prepare("SELECT COUNT(*) AS cnt FROM call_logs WHERE id = ?").get("fresh-log").cnt,
|
||||
(db.prepare("SELECT COUNT(*) AS cnt FROM call_logs WHERE id = ?").get("fresh-log") as any).cnt,
|
||||
1
|
||||
);
|
||||
assert.equal(fs.existsSync(freshAbsPath), true);
|
||||
@@ -365,9 +367,18 @@ test("getCallLogById falls back to legacy inline rows and request_detail_logs",
|
||||
assert.deepEqual(detail?.responseBody, { recovered: "response" });
|
||||
assert.deepEqual(detail?.error, { message: "legacy-error" });
|
||||
assert.equal(detail?.pipelinePayloads?.clientRequest?.body?.from, "detail-client");
|
||||
assert.equal(detail?.pipelinePayloads?.providerRequest?.body?.from, "detail-provider-request");
|
||||
assert.equal(detail?.pipelinePayloads?.providerResponse?.body?.from, "detail-provider-response");
|
||||
assert.equal(detail?.pipelinePayloads?.clientResponse?.body?.from, "detail-client-response");
|
||||
assert.equal(
|
||||
(detail?.pipelinePayloads?.providerRequest as any).body?.from,
|
||||
"detail-provider-request"
|
||||
);
|
||||
(assert as any).equal(
|
||||
(detail?.pipelinePayloads?.providerResponse as any).body?.from,
|
||||
"detail-provider-response"
|
||||
);
|
||||
assert.equal(
|
||||
(detail?.pipelinePayloads?.clientResponse as any).body?.from,
|
||||
"detail-client-response"
|
||||
);
|
||||
assert.equal(detail?.hasPipelineDetails, true);
|
||||
});
|
||||
|
||||
@@ -394,8 +405,8 @@ test("getCallLogById marks missing artifacts explicitly and clears stale DB poin
|
||||
const row = db
|
||||
.prepare("SELECT artifact_relpath, detail_state FROM call_logs WHERE id = ?")
|
||||
.get("missing-artifact");
|
||||
assert.equal(row.artifact_relpath, null);
|
||||
assert.equal(row.detail_state, "missing");
|
||||
assert.equal((row as any).artifact_relpath, null);
|
||||
assert.equal((row as any).detail_state, "missing");
|
||||
});
|
||||
|
||||
test("saveCallLog keeps large payloads out of SQLite while preserving explicit detail export", async () => {
|
||||
@@ -424,12 +435,12 @@ test("saveCallLog keeps large payloads out of SQLite while preserving explicit d
|
||||
`
|
||||
)
|
||||
.get("artifact-only-large-payload");
|
||||
assert.equal(row.detail_state, "ready");
|
||||
assert.equal(row.has_request_body, 1);
|
||||
assert.equal(typeof row.artifact_relpath, "string");
|
||||
assert.equal(row.error_summary, "upstream unavailable");
|
||||
assert.equal((row as any).detail_state, "ready");
|
||||
assert.equal((row as any).has_request_body, 1);
|
||||
(assert as any).equal(typeof (row as any).artifact_relpath, "string");
|
||||
assert.equal((row as any).error_summary, "upstream unavailable");
|
||||
|
||||
const artifactPath = path.join(TEST_DATA_DIR, "call_logs", row.artifact_relpath);
|
||||
const artifactPath = path.join(TEST_DATA_DIR, "call_logs", (row as any).artifact_relpath);
|
||||
const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8"));
|
||||
assert.equal(artifact.requestBody.payload.length, requestBody.payload.length);
|
||||
|
||||
@@ -438,7 +449,7 @@ test("saveCallLog keeps large payloads out of SQLite while preserving explicit d
|
||||
|
||||
const exported = await callLogs.exportCallLogsSince("2026-03-31T00:00:00.000Z");
|
||||
assert.equal(exported.length, 1);
|
||||
assert.equal(exported[0].requestBody.payload.length, requestBody.payload.length);
|
||||
assert.equal((exported[0] as any).requestBody.payload.length, requestBody.payload.length);
|
||||
});
|
||||
|
||||
test("saveCallLog logs and returns when sqlite persistence throws unexpectedly", async () => {
|
||||
|
||||
@@ -33,7 +33,7 @@ async function resetTestDataDir() {
|
||||
const db = core.getDbInstance();
|
||||
db.prepare("DELETE FROM call_logs").run();
|
||||
return;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
@@ -176,7 +176,7 @@ test("call log file rotation honors both retention days and file count", () => {
|
||||
|
||||
const db = core.getDbInstance();
|
||||
assert.equal(
|
||||
db.prepare("SELECT COUNT(*) AS cnt FROM call_logs WHERE id = ?").get("old-log").cnt,
|
||||
(db.prepare("SELECT COUNT(*) AS cnt FROM call_logs WHERE id = ?").get("old-log") as any).cnt,
|
||||
0
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(CALL_LOGS_DIR, oldRelPath)), false);
|
||||
@@ -184,8 +184,8 @@ test("call log file rotation honors both retention days and file count", () => {
|
||||
const keepARow = db
|
||||
.prepare("SELECT detail_state, artifact_relpath FROM call_logs WHERE id = ?")
|
||||
.get("keep-a");
|
||||
assert.equal(keepARow.detail_state, "missing");
|
||||
assert.equal(keepARow.artifact_relpath, null);
|
||||
assert.equal((keepARow as any).detail_state, "missing");
|
||||
(assert as any).equal((keepARow as any).artifact_relpath, null);
|
||||
assert.equal(fs.existsSync(path.join(CALL_LOGS_DIR, keepARelPath)), false);
|
||||
|
||||
assert.equal(fs.existsSync(path.join(CALL_LOGS_DIR, keepBRelPath)), true);
|
||||
|
||||
@@ -15,7 +15,7 @@ async function removeTestDataDir() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
return;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
|
||||
@@ -55,14 +55,14 @@ test("v1 models exposes CC-compatible fallback models under the provider node pr
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
const ids = new Set(body.data.map((item) => item.id));
|
||||
|
||||
assert.ok(ids.has("cm/claude-opus-4-7"));
|
||||
assert.ok(ids.has("cm/claude-opus-4-6"));
|
||||
assert.ok(ids.has("cm/claude-sonnet-4-6"));
|
||||
assert.equal(
|
||||
[...ids].some((id) => id.startsWith("anthropic-compatible-cc-cm/")),
|
||||
[...ids].some((id) => (id as any).startsWith("anthropic-compatible-cc-cm/")),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
@@ -117,15 +117,15 @@ test("buildClaudeCodeCompatibleRequest keeps prior role history while dropping t
|
||||
{ role: "user", text: "u2" },
|
||||
]
|
||||
);
|
||||
assert.equal(payload.messages[0].content.at(-1).cache_control, undefined);
|
||||
assert.equal(payload.messages[1].content.at(-1).cache_control, undefined);
|
||||
assert.equal(payload.messages[2].content.at(-1).cache_control, undefined);
|
||||
assert.equal((payload.messages[0].content.at(-1) as any).cache_control, undefined);
|
||||
assert.equal((payload.messages[1] as any).content.at(-1).cache_control, undefined);
|
||||
assert.equal((payload.messages as any)[2].content.at(-1).cache_control, undefined);
|
||||
assert.equal(payload.system.length, 2);
|
||||
assert.match(payload.system[0].text, /Claude Agent SDK/);
|
||||
assert.equal(payload.system[0].cache_control, undefined);
|
||||
assert.equal(payload.system[1].cache_control, undefined);
|
||||
assert.match((payload as any).system[0].text, /Claude Agent SDK/);
|
||||
(assert as any).equal((payload.system[0] as any).cache_control, undefined);
|
||||
assert.equal((payload.system[1] as any).cache_control, undefined);
|
||||
assert.equal(payload.system[1].text, "sys");
|
||||
assert.equal(payload.system[1].cache_control, undefined);
|
||||
assert.equal((payload.system[1] as any).cache_control, undefined);
|
||||
assert.equal(payload.tools.length, 1);
|
||||
assert.deepEqual(payload.tools[0], {
|
||||
name: "lookup_weather",
|
||||
@@ -139,8 +139,8 @@ test("buildClaudeCodeCompatibleRequest keeps prior role history while dropping t
|
||||
},
|
||||
});
|
||||
assert.deepEqual(payload.tool_choice, { type: "any" });
|
||||
assert.equal(payload.context_management, undefined);
|
||||
assert.equal(JSON.parse(payload.metadata.user_id).session_id, "session-1");
|
||||
assert.equal(payload.context_management, undefined as any);
|
||||
assert.equal(JSON.parse((payload as any).metadata.user_id).session_id, "session-1");
|
||||
});
|
||||
|
||||
test("buildClaudeCodeCompatibleRequest preserves xhigh for Claude models that support it", () => {
|
||||
@@ -212,13 +212,15 @@ test("buildClaudeCodeCompatibleRequest preserves Claude cache markers when reque
|
||||
preserveCacheControl: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(payload.system[0].cache_control, { type: "ephemeral", ttl: "5m" });
|
||||
assert.deepEqual(payload.messages[0].content[0].cache_control, { type: "ephemeral" });
|
||||
assert.deepEqual(payload.messages[1].content[0].cache_control, {
|
||||
assert.deepEqual((payload.system[0] as any).cache_control, { type: "ephemeral", ttl: "5m" });
|
||||
(assert as any).deepEqual((payload.messages[0].content[0] as any).cache_control, {
|
||||
type: "ephemeral",
|
||||
});
|
||||
assert.deepEqual((payload.messages[1].content[0] as any).cache_control, {
|
||||
type: "ephemeral",
|
||||
ttl: "10m",
|
||||
});
|
||||
assert.equal(payload.messages[2].content[0].cache_control, undefined);
|
||||
assert.equal((payload.messages[2].content[0] as any).cache_control, undefined);
|
||||
assert.deepEqual(payload.tools[0].cache_control, { type: "ephemeral", ttl: "30m" });
|
||||
});
|
||||
|
||||
@@ -263,11 +265,11 @@ test("buildClaudeCodeCompatibleRequest does not supplement missing Claude cache
|
||||
preserveCacheControl: true,
|
||||
});
|
||||
|
||||
assert.equal(payload.system[0].cache_control, undefined);
|
||||
assert.equal(payload.messages[0].content[0].cache_control, undefined);
|
||||
assert.equal(payload.messages[1].content[0].cache_control, undefined);
|
||||
assert.equal(payload.messages[2].content[0].cache_control, undefined);
|
||||
assert.equal(payload.system.at(-1).cache_control, undefined);
|
||||
assert.equal((payload.system[0] as any).cache_control, undefined);
|
||||
assert.equal((payload.messages[0].content[0] as any).cache_control, undefined);
|
||||
assert.equal((payload.messages[1].content[0] as any).cache_control, undefined);
|
||||
assert.equal((payload.messages[2].content[0] as any).cache_control, undefined);
|
||||
assert.equal((payload.system.at(-1) as any).cache_control, undefined);
|
||||
assert.equal(payload.tools[0].cache_control, undefined);
|
||||
});
|
||||
|
||||
@@ -293,8 +295,8 @@ test("buildClaudeCodeCompatibleRequest keeps built-in system blocks untagged whe
|
||||
preserveCacheControl: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(payload.system[0].cache_control, { type: "ephemeral" });
|
||||
assert.deepEqual(payload.system[1].cache_control, { type: "ephemeral", ttl: "1h" });
|
||||
assert.deepEqual((payload.system[0] as any).cache_control, { type: "ephemeral" });
|
||||
assert.deepEqual((payload.system[1] as any).cache_control, { type: "ephemeral", ttl: "1h" });
|
||||
});
|
||||
|
||||
test("buildClaudeCodeCompatibleRequest does not add cache markers in non-preserve mode", () => {
|
||||
@@ -321,12 +323,12 @@ test("buildClaudeCodeCompatibleRequest does not add cache markers in non-preserv
|
||||
preserveCacheControl: false,
|
||||
});
|
||||
|
||||
assert.equal(payload.system.length, 2);
|
||||
assert.equal(payload.system[0].cache_control, undefined);
|
||||
assert.equal(payload.system[1].cache_control, undefined);
|
||||
assert.equal(payload.messages[0].content[0].cache_control, undefined);
|
||||
assert.equal(payload.messages[1].content[0].cache_control, undefined);
|
||||
assert.equal(payload.messages[2].content[0].cache_control, undefined);
|
||||
(assert as any).equal(payload.system.length, 2);
|
||||
assert.equal((payload as any).system[0].cache_control, undefined);
|
||||
assert.equal((payload as any).system[1].cache_control, undefined);
|
||||
assert.equal((payload as any).messages[0].content[0].cache_control, undefined);
|
||||
assert.equal((payload as any).messages[1].content[0].cache_control, undefined);
|
||||
assert.equal((payload.messages[2].content[0] as any).cache_control, undefined);
|
||||
});
|
||||
|
||||
test("buildClaudeCodeCompatibleRequest falls back to a user turn when the source only has assistant/model text", () => {
|
||||
@@ -557,7 +559,7 @@ test("handleChatCore forces SSE upstream for CC compatible providers while retur
|
||||
assert.equal(calls[0].body.stream, true);
|
||||
assert.equal(JSON.stringify(calls[0].body).includes('"cache_control"'), false);
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(payload.choices[0].message.content, "Hello from CC");
|
||||
assert.equal(payload.choices[0].finish_reason, "stop");
|
||||
assert.equal(payload.usage.prompt_tokens, 2007);
|
||||
@@ -725,7 +727,7 @@ test("provider-nodes create route creates CC node with dedicated prefix when ena
|
||||
);
|
||||
|
||||
assert.equal(response.status, 201);
|
||||
const data = await response.json();
|
||||
const data = (await response.json()) as any;
|
||||
assert.match(data.node.id, /^anthropic-compatible-cc-/);
|
||||
assert.equal(data.node.baseUrl, "https://proxy.example.com");
|
||||
assert.equal(data.node.chatPath, CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH);
|
||||
@@ -780,7 +782,7 @@ test("provider-nodes validate route rejects invalid JSON and schema errors", asy
|
||||
);
|
||||
|
||||
assert.equal(invalidBodyResponse.status, 400);
|
||||
const invalidBodyPayload = await invalidBodyResponse.json();
|
||||
const invalidBodyPayload = (await invalidBodyResponse.json()) as any;
|
||||
assert.equal(invalidBodyPayload.error.message, "Invalid request");
|
||||
assert.equal(invalidBodyPayload.error.details.length >= 1, true);
|
||||
});
|
||||
@@ -1053,7 +1055,7 @@ test("provider-nodes list route exposes CC flag state from server env", async ()
|
||||
const response = await providerNodesRoute.GET();
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const data = await response.json();
|
||||
const data = (await response.json()) as any;
|
||||
assert.equal(data.ccCompatibleProviderEnabled, true);
|
||||
});
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ test("combo live test bypasses connection cooldown and breaker state to perform
|
||||
const liveResponse = await chatRoute.POST(
|
||||
makeRequest({ "X-Internal-Test": "combo-health-check" })
|
||||
);
|
||||
const liveBody = await liveResponse.json();
|
||||
const liveBody = (await liveResponse.json()) as any;
|
||||
|
||||
assert.equal(liveResponse.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
@@ -130,7 +130,7 @@ test("combo live test bypasses connection cooldown and breaker state to perform
|
||||
assert.equal(fetchCalls[0].init.headers.Authorization, "Bearer sk-live-test");
|
||||
assert.equal(liveBody.choices[0].message.content, "OK");
|
||||
|
||||
const updated = await providersDb.getProviderConnectionById(created.id);
|
||||
const updated = await providersDb.getProviderConnectionById((created as any).id);
|
||||
assert.equal(updated.testStatus, "active");
|
||||
});
|
||||
|
||||
@@ -174,7 +174,7 @@ test("combo live test bypasses semantic cache and forces a fresh upstream reques
|
||||
|
||||
try {
|
||||
const cachedResponse = await chatRoute.POST(makeRequest());
|
||||
const cachedBody = await cachedResponse.json();
|
||||
const cachedBody = (await cachedResponse.json()) as any;
|
||||
|
||||
assert.equal(cachedResponse.status, 200);
|
||||
assert.equal(fetchCalls.length, 0);
|
||||
@@ -187,7 +187,7 @@ test("combo live test bypasses semantic cache and forces a fresh upstream reques
|
||||
"X-Request-Id": "combo-test-cache-bypass",
|
||||
})
|
||||
);
|
||||
const liveBody = await liveResponse.json();
|
||||
const liveBody = (await liveResponse.json()) as any;
|
||||
|
||||
assert.equal(liveResponse.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
|
||||
@@ -159,7 +159,7 @@ test("handleChat generates and injects context-relay handoffs across Codex accou
|
||||
},
|
||||
})
|
||||
);
|
||||
const firstJson = await firstResponse.json();
|
||||
const firstJson = (await firstResponse.json()) as any;
|
||||
|
||||
assert.equal(firstResponse.status, 200);
|
||||
assert.equal(firstJson.choices[0].message.content, "relay-success");
|
||||
@@ -171,7 +171,7 @@ test("handleChat generates and injects context-relay handoffs across Codex accou
|
||||
assert.equal(summaryBodies.length, 1);
|
||||
assert.match(summaryBodies[0].serializedBody, /You are a context summarizer/);
|
||||
|
||||
await providersDb.updateProviderConnection(primary.id, {
|
||||
await providersDb.updateProviderConnection((primary as any).id, {
|
||||
rateLimitedUntil: new Date(Date.now() + 60_000).toISOString(),
|
||||
});
|
||||
|
||||
@@ -185,7 +185,7 @@ test("handleChat generates and injects context-relay handoffs across Codex accou
|
||||
},
|
||||
})
|
||||
);
|
||||
const secondJson = await secondResponse.json();
|
||||
const secondJson = (await secondResponse.json()) as any;
|
||||
|
||||
assert.equal(secondResponse.status, 200);
|
||||
assert.equal(secondJson.choices[0].message.content, "relay-success");
|
||||
|
||||
@@ -75,7 +75,7 @@ test("handleChat waits for a short cooldown and retries once within the configur
|
||||
})
|
||||
);
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls, 1);
|
||||
@@ -126,7 +126,7 @@ test("handleChat recovers from a real 429 once the connection cooldown expires",
|
||||
})
|
||||
);
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls, 4);
|
||||
@@ -161,7 +161,7 @@ test("handleChat does not wait when the cooldown exceeds maxRetryIntervalSec", a
|
||||
},
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(fetchCalls, 0);
|
||||
assert.equal(response.status, 503);
|
||||
@@ -181,9 +181,15 @@ test("handleChat returns model_cooldown when every credential for the requested
|
||||
maxRetryIntervalSec: 0,
|
||||
});
|
||||
|
||||
await auth.markAccountUnavailable(first.id, 429, "too many requests", "gemini", "gemini-2.5-pro");
|
||||
await auth.markAccountUnavailable(
|
||||
second.id,
|
||||
(first as any).id,
|
||||
429,
|
||||
"too many requests",
|
||||
"gemini",
|
||||
"gemini-2.5-pro"
|
||||
);
|
||||
await auth.markAccountUnavailable(
|
||||
(second as any).id,
|
||||
429,
|
||||
"too many requests",
|
||||
"gemini",
|
||||
@@ -205,7 +211,7 @@ test("handleChat returns model_cooldown when every credential for the requested
|
||||
},
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(fetchCalls, 0);
|
||||
assert.equal(response.status, 429);
|
||||
@@ -247,7 +253,7 @@ test("handleChat aborts the pending cooldown wait when the client disconnects",
|
||||
controller.signal
|
||||
)
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(fetchCalls, 0);
|
||||
assert.equal(response.status, 499);
|
||||
|
||||
@@ -58,7 +58,7 @@ test("resolveModelOrError rejects ambiguous aliases without a provider prefix",
|
||||
|
||||
assert.ok(result.error);
|
||||
assert.equal(result.error.status, 400);
|
||||
const json = await result.error.json();
|
||||
const json = (await result.error.json()) as any;
|
||||
assert.match(json.error.message, /Ambiguous model/i);
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ test("resolveModelOrError rejects ambiguous slashful canonical ids instead of mi
|
||||
|
||||
assert.ok(result.error);
|
||||
assert.equal(result.error.status, 400);
|
||||
const json = await result.error.json();
|
||||
const json = (await result.error.json()) as any;
|
||||
assert.match(json.error.message, /Ambiguous model/i);
|
||||
assert.match(json.error.message, /openai\/gpt-oss-120b/i);
|
||||
});
|
||||
@@ -85,7 +85,7 @@ test("resolveModelOrError rejects malformed model strings", async () => {
|
||||
|
||||
assert.ok(result.error);
|
||||
assert.equal(result.error.status, 400);
|
||||
const json = await result.error.json();
|
||||
const json = (await result.error.json()) as any;
|
||||
assert.match(json.error.message, /Invalid model format/i);
|
||||
});
|
||||
|
||||
@@ -101,7 +101,7 @@ test("checkPipelineGates blocks providers with an open circuit breaker", async (
|
||||
resetTimeoutMs: 5_000,
|
||||
},
|
||||
});
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
const retryAfter = Number(response.headers.get("Retry-After"));
|
||||
|
||||
assert.equal(response.status, 503);
|
||||
@@ -143,8 +143,8 @@ test("handleNoCredentials reports missing provider credentials and exhausted acc
|
||||
500
|
||||
);
|
||||
|
||||
const missingJson = await missing.json();
|
||||
const exhaustedJson = await exhausted.json();
|
||||
const missingJson = (await missing.json()) as any;
|
||||
const exhaustedJson = (await exhausted.json()) as any;
|
||||
|
||||
assert.equal(missing.status, 400);
|
||||
assert.match(missingJson.error.message, /No credentials for provider: openai/);
|
||||
@@ -168,7 +168,7 @@ test("handleNoCredentials returns Retry-After when every account is rate limited
|
||||
null,
|
||||
null
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 429);
|
||||
assert.ok(Number(response.headers.get("Retry-After")) >= 1);
|
||||
@@ -193,7 +193,7 @@ test("handleNoCredentials returns structured model_cooldown when every credentia
|
||||
null,
|
||||
null
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 429);
|
||||
assert.equal(Number(response.headers.get("Retry-After")) >= 1, true);
|
||||
@@ -207,7 +207,7 @@ test("handleNoCredentials returns structured model_cooldown when every credentia
|
||||
test("safeResolveProxy returns the direct route when no proxy config is present", async () => {
|
||||
const connection = await seedConnection("openai", { apiKey: "sk-openai-direct" });
|
||||
|
||||
const resolved = await safeResolveProxy(connection.id);
|
||||
const resolved = await safeResolveProxy((connection as any).id);
|
||||
|
||||
assert.deepEqual(resolved, {
|
||||
proxy: null,
|
||||
@@ -230,7 +230,10 @@ test("executeChatWithBreaker converts proxy fast-fail errors", async () => {
|
||||
apiKey: "sk-openai-helper",
|
||||
providerSpecificData: {},
|
||||
};
|
||||
const breaker = getCircuitBreaker("openai");
|
||||
const proxyResult = await executeChatWithBreaker({
|
||||
bypassCircuitBreaker: false,
|
||||
breaker,
|
||||
body: { model: "openai/gpt-4o-mini" },
|
||||
provider: "openai",
|
||||
model: "gpt-4o-mini",
|
||||
|
||||
@@ -54,7 +54,7 @@ test("handleChat applies body-derived retry-after to the runtime limiter", async
|
||||
},
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 429);
|
||||
assert.match(body.error.message, /retry after 20s/i);
|
||||
@@ -86,7 +86,7 @@ test("handleChat tolerates non-JSON rate-limit bodies without breaking fallback
|
||||
},
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 429);
|
||||
assert.match(body.error.message, /rate limit exceeded but body is not json/i);
|
||||
|
||||
@@ -84,7 +84,7 @@ test("handleChat returns 400 for malformed JSON payloads", async () => {
|
||||
body: "{bad-json",
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(json.error.message, /Invalid JSON body/i);
|
||||
@@ -106,7 +106,7 @@ test("handleChat rejects suspicious prompt-injection payloads before routing", a
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(json.error.message, /suspicious content detected/i);
|
||||
@@ -132,7 +132,7 @@ test("handleChat redacts PII before sending the upstream request", async () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
@@ -199,7 +199,7 @@ test("handleChat rejects requests without a model", async () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(json.error.message, /Missing model/i);
|
||||
@@ -232,7 +232,7 @@ test("handleChat applies task-aware routing when a semantic override is enabled"
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(seenAuthHeaders, ["Bearer sk-deepseek-task-route"]);
|
||||
@@ -279,7 +279,7 @@ test("handleChat routes exact combo names and can recover via global fallback",
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(attempts, 2);
|
||||
@@ -320,7 +320,7 @@ test("handleChat keeps the combo error when the global fallback throws", async (
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 503);
|
||||
assert.equal(attempts, 2);
|
||||
@@ -337,7 +337,7 @@ test("handleChat returns 400 when no provider credentials exist", async () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(json.error.message, /No credentials for provider: openai/);
|
||||
@@ -358,7 +358,7 @@ test("handleChat returns 503 for cooled-down connections and 503 for open circui
|
||||
},
|
||||
})
|
||||
);
|
||||
const cooldownJson = await cooldownResponse.json();
|
||||
const cooldownJson = (await cooldownResponse.json()) as any;
|
||||
assert.equal(cooldownResponse.status, 503);
|
||||
assert.ok(Number(cooldownResponse.headers.get("Retry-After")) >= 1);
|
||||
assert.match(cooldownJson.error.message, /\[openai\/gpt-4o-mini\]/i);
|
||||
@@ -377,7 +377,7 @@ test("handleChat returns 503 for cooled-down connections and 503 for open circui
|
||||
},
|
||||
})
|
||||
);
|
||||
const breakerJson = await breakerBlocked.json();
|
||||
const breakerJson = (await breakerBlocked.json()) as any;
|
||||
|
||||
assert.equal(breakerBlocked.status, 503);
|
||||
assert.equal(breakerBlocked.headers.get("X-OmniRoute-Provider-Breaker"), "open");
|
||||
@@ -403,7 +403,7 @@ test("handleChat maps upstream timeouts to HTTP 504", async () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 504);
|
||||
assert.match(json.error.message, /\[504\]: upstream timed out/);
|
||||
@@ -440,7 +440,7 @@ test("handleChat uses the emergency fallback model on budget exhaustion", async
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(seenBodies.length, 2);
|
||||
@@ -483,7 +483,7 @@ test("handleChat returns the primary budget error when emergency fallback also f
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 402);
|
||||
assert.deepEqual(seenModels, ["gpt-4o-mini", "openai/gpt-oss-120b", "openai/gpt-oss-120b"]);
|
||||
@@ -506,7 +506,7 @@ test("handleChat rejects models that are not allowed by the caller API key polic
|
||||
},
|
||||
})
|
||||
);
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 403);
|
||||
assert.match(json.error.message, /not allowed|model restriction|forbidden/i);
|
||||
|
||||
@@ -115,7 +115,7 @@ test("Test 3: handleChat returns cached response directly for Semantic Cache hit
|
||||
});
|
||||
|
||||
const res2 = await handleChat(req2);
|
||||
const json2 = await res2.json();
|
||||
const json2 = (await res2.json()) as any;
|
||||
|
||||
assert.equal(fetchCount, 1, "Should have hit the semantic cache without calling fetch again");
|
||||
assert.equal(json2.choices[0].message.content, "Cache Generation 1");
|
||||
@@ -207,7 +207,7 @@ test("handleChat returns cached response directly for Idempotency hits", async (
|
||||
})
|
||||
);
|
||||
|
||||
const json2 = await response2.json();
|
||||
const json2 = (await response2.json()) as any;
|
||||
assert.equal(response2.status, 200);
|
||||
assert.equal(response2.headers.get("X-OmniRoute-Idempotent"), "true");
|
||||
assert.equal(json2.choices[0].message.content, "Original response");
|
||||
@@ -243,7 +243,7 @@ test("Test 6: handleChat correctly sets isResponsesEndpoint for /v1/responses",
|
||||
})
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 200);
|
||||
const responseText = json.output_text || json.output?.[0]?.content?.[0]?.text;
|
||||
assert.equal(responseText, "Responses OK");
|
||||
@@ -269,7 +269,7 @@ test("handleChat returns Semantic Cache hit", async () => {
|
||||
// Second request: should hit semantic cache
|
||||
const response2 = await handleChat(buildRequest({ body: reqBody }));
|
||||
|
||||
const json2 = await response2.json();
|
||||
const json2 = (await response2.json()) as any;
|
||||
assert.equal(response2.status, 200);
|
||||
assert.equal(response2.headers.get("X-OmniRoute-Cache"), "HIT");
|
||||
assert.equal(json2.choices[0].message.content, "Semantic API response");
|
||||
|
||||
@@ -41,7 +41,7 @@ test("chatCore integration: compressContext called proactively when context exce
|
||||
`Final tokens ${result.stats.final} should fit within limit ${contextLimit}`
|
||||
);
|
||||
assert.equal(
|
||||
result.body.messages[result.body.messages.length - 1].content,
|
||||
result.body.messages[(result.body.messages as any).length - 1].content,
|
||||
"Final question?",
|
||||
"Latest user turn should be preserved after compression"
|
||||
);
|
||||
@@ -133,7 +133,7 @@ test("chatCore integration: compression handles tool messages", async () => {
|
||||
|
||||
assert.ok(result.compressed, "Context should be compressed");
|
||||
|
||||
const toolMessage = result.body.messages.find((m: any) => m.role === "tool");
|
||||
const toolMessage = (result.body as any).messages.find((m: any) => m.role === "tool");
|
||||
assert.ok(toolMessage, "Tool message should exist");
|
||||
assert.ok(toolMessage.content.length < longToolOutput.length, "Tool message should be truncated");
|
||||
assert.ok(
|
||||
|
||||
@@ -404,7 +404,7 @@ test("chatCore honors providerSpecificData.apiType for legacy openai-compatible
|
||||
responseFormat: "openai-responses",
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(result.success, true);
|
||||
assert.match(call.url, /\/responses$/);
|
||||
assert.ok(call.body.input);
|
||||
@@ -770,7 +770,7 @@ test("chatCore restores prefixed Claude passthrough tool names in upstream respo
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(payload.content[0].name, "Bash");
|
||||
});
|
||||
@@ -850,7 +850,7 @@ test("chatCore surfaces typed translation errors with the declared error type",
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 422);
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(payload.error.type, "unsupported_feature");
|
||||
assert.equal(payload.error.code, "unsupported_feature");
|
||||
});
|
||||
@@ -931,7 +931,7 @@ test("chatCore refreshes GitHub credentials after 401 and retries with the refre
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
const providerCalls = calls.filter((entry) =>
|
||||
entry.url.startsWith("https://api.githubcopilot.com/")
|
||||
);
|
||||
@@ -1134,7 +1134,7 @@ test("chatCore serves a cached idempotent response without hitting the provider
|
||||
assert.equal(second.result.success, true);
|
||||
assert.equal(second.result.response.headers.get("X-OmniRoute-Idempotent"), "true");
|
||||
|
||||
const payload = await second.result.response.json();
|
||||
const payload = (await second.result.response.json()) as any;
|
||||
assert.equal(payload.choices[0].message.content, "ok");
|
||||
});
|
||||
|
||||
@@ -1175,7 +1175,7 @@ test("chatCore returns a semantic cache HIT for repeated deterministic requests"
|
||||
assert.equal(second.result.response.headers.get("X-OmniRoute-Cache"), "HIT");
|
||||
assert.equal(upstreamHits, 1);
|
||||
|
||||
const payload = await second.result.response.json();
|
||||
const payload = (await second.result.response.json()) as any;
|
||||
assert.equal(payload.choices[0].message.content, "cached-once");
|
||||
|
||||
await waitForAsyncSideEffects();
|
||||
@@ -1230,7 +1230,7 @@ test("chatCore skips semantic cache when disabled in settings", async () => {
|
||||
assert.equal(first.result.response.headers.get("X-OmniRoute-Cache"), "MISS");
|
||||
assert.equal(second.result.response.headers.get("X-OmniRoute-Cache"), "MISS");
|
||||
|
||||
const payload = await second.result.response.json();
|
||||
const payload = (await second.result.response.json()) as any;
|
||||
assert.equal(payload.choices[0].message.content, "fresh-2");
|
||||
});
|
||||
|
||||
@@ -1301,7 +1301,7 @@ test("chatCore normalizes tool finish reasons and estimates usage when upstream
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(payload.choices[0].finish_reason, "tool_calls");
|
||||
assert.ok(payload.usage.total_tokens > 0);
|
||||
@@ -1319,7 +1319,7 @@ test("chatCore bypasses Claude CLI warmup probes before touching the provider",
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(calls.length, 0);
|
||||
assert.match(payload.choices[0].message.content, /CLI Command Execution/);
|
||||
@@ -1380,7 +1380,7 @@ test("chatCore retries Qwen quota 429 responses before succeeding", async () =>
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(payload.choices[0].message.content, "qwen recovered");
|
||||
@@ -1489,13 +1489,16 @@ test("chatCore persists Codex quota headers and scope cooldown on 429 responses"
|
||||
},
|
||||
});
|
||||
|
||||
const updated = await providersDb.getProviderConnectionById(connection.id);
|
||||
const updated = await providersDb.getProviderConnectionById((connection as any).id);
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 429);
|
||||
assert.equal(updated.providerSpecificData.codexQuotaState.limit5h, 100);
|
||||
assert.equal(updated.providerSpecificData.codexQuotaState.scope, "codex");
|
||||
assert.equal(typeof updated.providerSpecificData.codexScopeRateLimitedUntil.codex, "string");
|
||||
assert.equal(updated.providerSpecificData.codexExhaustedWindow, "5h");
|
||||
assert.equal((updated as any).providerSpecificData.codexQuotaState.limit5h, 100);
|
||||
assert.equal((updated as any).providerSpecificData.codexQuotaState.scope, "codex");
|
||||
assert.equal(
|
||||
typeof (updated as any).providerSpecificData.codexScopeRateLimitedUntil.codex,
|
||||
"string"
|
||||
);
|
||||
(assert as any).equal((updated.providerSpecificData as any).codexExhaustedWindow, "5h");
|
||||
});
|
||||
|
||||
test("chatCore falls back to the next family model when the requested model is unavailable", async () => {
|
||||
@@ -1518,7 +1521,7 @@ test("chatCore falls back to the next family model when the requested model is u
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls[1].body.model, "gpt-5.1-mini");
|
||||
@@ -1553,7 +1556,7 @@ test("chatCore falls back to a larger-context sibling when the request overflows
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls[1].body.model, "gpt-4o");
|
||||
@@ -1574,7 +1577,7 @@ test("chatCore parses upstream SSE payloads for non-streaming requests", async (
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(payload.choices[0].message.content, "sse json");
|
||||
});
|
||||
@@ -1657,7 +1660,7 @@ test("chatCore falls back after an empty-content success response", async () =>
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls[1].body.model, "gpt-5.1-mini");
|
||||
@@ -1813,7 +1816,7 @@ test("chatCore serves emergency fallback responses for budget errors on non-stre
|
||||
},
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(calls.length, 2);
|
||||
@@ -1938,7 +1941,7 @@ test("chatCore caches streaming response and serves cache HIT on repeat", async
|
||||
assert.equal(second.calls.length, 0, "second request should not reach upstream");
|
||||
assert.equal(second.result.response.headers.get("X-OmniRoute-Cache"), "HIT");
|
||||
|
||||
const payload = await second.result.response.json();
|
||||
const payload = (await second.result.response.json()) as any;
|
||||
assert.ok(payload.choices, "cached response should have choices");
|
||||
assert.equal(payload.choices[0].message.content, "streamed-once");
|
||||
});
|
||||
@@ -2070,6 +2073,6 @@ test("chatCore returns cache HIT as JSON even when client requests SSE", async (
|
||||
"cache HIT should return JSON regardless of stream flag"
|
||||
);
|
||||
|
||||
const payload = await second.result.response.json();
|
||||
const payload = (await second.result.response.json()) as any;
|
||||
assert.equal(payload.choices[0].message.content, "cached-json");
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ test("buildClaudeCodeCompatibleValidationPayload produces the expected smoke-tes
|
||||
});
|
||||
assert.equal(payload.tools.length, 0);
|
||||
assert.equal(payload.system.length, 1);
|
||||
assert.match(payload.system[0].text, /Claude Agent SDK/);
|
||||
assert.ok(JSON.parse(payload.metadata.user_id).session_id);
|
||||
assert.match((payload as any).system[0].text, /Claude Agent SDK/);
|
||||
assert.ok((JSON as any).parse(payload.metadata.user_id).session_id);
|
||||
assert.ok(CLAUDE_CODE_COMPATIBLE_DEFAULT_MAX_TOKENS > payload.max_tokens);
|
||||
});
|
||||
|
||||
@@ -115,7 +115,7 @@ test("buildClaudeCodeCompatibleRequest covers normalized OpenAI-style messages,
|
||||
},
|
||||
]);
|
||||
assert.equal(payload.system.length, 3);
|
||||
assert.match(payload.system[0].text, /Claude Agent SDK/);
|
||||
assert.match((payload as any).system[0].text, /Claude Agent SDK/);
|
||||
assert.equal(payload.system[1].text, "system note");
|
||||
assert.equal(payload.system[2].text, "developer note");
|
||||
assert.equal(payload.tools.length, 1);
|
||||
@@ -194,17 +194,17 @@ test("buildClaudeCodeCompatibleRequest covers Claude-native bodies and cache-con
|
||||
});
|
||||
|
||||
assert.equal(stripped.stream, true);
|
||||
assert.equal(JSON.parse(stripped.metadata.user_id).session_id, "explicit-session");
|
||||
assert.equal((JSON as any).parse(stripped.metadata.user_id).session_id, "explicit-session");
|
||||
assert.equal(stripped.messages.at(-1).role, "user");
|
||||
assert.equal(stripped.system[0].cache_control, undefined);
|
||||
assert.equal(stripped.messages[0].content[0].cache_control, undefined);
|
||||
assert.equal((stripped as any).system[0].cache_control, undefined);
|
||||
assert.equal((stripped as any).messages[0].content[0].cache_control, undefined);
|
||||
assert.equal(stripped.tools[0].cache_control, undefined);
|
||||
assert.deepEqual(stripped.thinking, { type: "enabled", budget_tokens: 12 });
|
||||
assert.deepEqual(stripped.output_config, { effort: "high", format: "compact" });
|
||||
assert.equal(stripped.metadata.foo, "bar");
|
||||
assert.deepEqual(preserved.system[0].cache_control, { type: "ephemeral" });
|
||||
assert.equal(preserved.messages[0].content[0].cache_control.type, "ephemeral");
|
||||
assert.equal(preserved.tools[0].cache_control.type, "ephemeral");
|
||||
(assert as any).deepEqual((preserved.system[0] as any).cache_control, { type: "ephemeral" });
|
||||
(assert as any).equal((preserved.messages[0].content[0] as any).cache_control.type, "ephemeral");
|
||||
assert.equal((preserved.tools[0].cache_control as any).type, "ephemeral");
|
||||
});
|
||||
|
||||
test("buildClaudeCodeCompatibleRequest omits tool choice when there are no tools", () => {
|
||||
@@ -222,8 +222,8 @@ test("buildClaudeCodeCompatibleRequest omits tool choice when there are no tools
|
||||
assert.equal(payload.tools.length, 0);
|
||||
assert.equal("tool_choice" in payload, false);
|
||||
assert.equal(payload.output_config.effort, "high");
|
||||
assert.equal(payload.system.length, 1);
|
||||
assert.match(payload.system[0].text, /Claude Agent SDK/);
|
||||
(assert as any).equal(payload.system.length, 1);
|
||||
assert.match((payload as any).system[0].text, /Claude Agent SDK/);
|
||||
});
|
||||
|
||||
test("buildClaudeCodeCompatibleRequest covers string system input, non-array Claude fields and tool choice variants", () => {
|
||||
@@ -264,8 +264,8 @@ test("buildClaudeCodeCompatibleRequest covers string system input, non-array Cla
|
||||
});
|
||||
|
||||
assert.deepEqual(anyChoice.tool_choice, { type: "any" });
|
||||
assert.equal(anyChoice.tools.length, 2);
|
||||
assert.equal(anyChoice.tools[0].input_schema.properties.q.type, "string");
|
||||
assert.equal((anyChoice as any).tools.length, 2);
|
||||
assert.equal((anyChoice.tools[0].input_schema as any).properties.q.type, "string");
|
||||
assert.equal(anyChoice.tools[1].description, "");
|
||||
assert.equal(stringSystem.messages.length, 0);
|
||||
assert.equal(stringSystem.tools.length, 0);
|
||||
|
||||
@@ -50,7 +50,7 @@ test.afterEach(() => {
|
||||
restoreEnv();
|
||||
|
||||
for (const dir of tempDirs) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
fs.rmSync(dir as any, { recursive: true, force: true });
|
||||
}
|
||||
tempDirs.clear();
|
||||
});
|
||||
|
||||
76
tests/unit/cli-tools-auth-hardening.test.ts
Normal file
76
tests/unit/cli-tools-auth-hardening.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const THIS_FILE = fileURLToPath(import.meta.url);
|
||||
const TESTS_DIR = path.dirname(THIS_FILE);
|
||||
const REPO_ROOT = path.resolve(TESTS_DIR, "../..");
|
||||
const CLI_TOOLS_DIR = path.join(REPO_ROOT, "src", "app", "api", "cli-tools");
|
||||
|
||||
function listCliToolRouteFiles(dir: string): string[] {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...listCliToolRouteFiles(fullPath));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && entry.name === "route.ts") {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
test("all cli-tools route handlers require the shared management auth guard", () => {
|
||||
const routeFiles = listCliToolRouteFiles(CLI_TOOLS_DIR)
|
||||
.map((fullPath) => path.relative(REPO_ROOT, fullPath).replace(/\\/g, "/"))
|
||||
.sort();
|
||||
|
||||
assert.ok(routeFiles.length > 0, "expected at least one cli-tools route");
|
||||
|
||||
for (const relPath of routeFiles) {
|
||||
const fullPath = path.join(REPO_ROOT, relPath);
|
||||
const content = fs.readFileSync(fullPath, "utf8");
|
||||
const handlerCount = (content.match(/export async function (GET|POST|PUT|DELETE)\(/g) || [])
|
||||
.length;
|
||||
const authCount = (
|
||||
content.match(/const authError = await requireCliToolsAuth\(request\);/g) || []
|
||||
).length;
|
||||
const returnCount = (content.match(/if \(authError\) return authError;/g) || []).length;
|
||||
|
||||
assert.ok(handlerCount > 0, `${relPath} should export at least one route handler`);
|
||||
assert.ok(
|
||||
content.includes('from "@/lib/api/requireCliToolsAuth"'),
|
||||
`${relPath} should import requireCliToolsAuth`
|
||||
);
|
||||
assert.equal(
|
||||
authCount,
|
||||
handlerCount,
|
||||
`${relPath} should guard every exported handler before host access`
|
||||
);
|
||||
assert.equal(
|
||||
returnCount,
|
||||
handlerCount,
|
||||
`${relPath} should return the auth error from every exported handler`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("cli-tools auth helper delegates to management auth", () => {
|
||||
const helperPath = path.join(REPO_ROOT, "src/lib/api/requireCliToolsAuth.ts");
|
||||
const content = fs.readFileSync(helperPath, "utf8");
|
||||
|
||||
assert.ok(
|
||||
content.includes('from "@/lib/api/requireManagementAuth"'),
|
||||
"requireCliToolsAuth should reuse the shared management auth helper"
|
||||
);
|
||||
assert.ok(
|
||||
content.includes("return requireManagementAuth(request);"),
|
||||
"requireCliToolsAuth should delegate directly to requireManagementAuth"
|
||||
);
|
||||
});
|
||||
@@ -210,8 +210,8 @@ test("cloudSync syncs data upstream and refreshes only locally stale provider to
|
||||
|
||||
const cloudSync = await loadCloudSync("sync-success");
|
||||
const result = await cloudSync.syncToCloud("machine-1", "created-key-1");
|
||||
const staleAfter = await providersDb.getProviderConnectionById(stale.id);
|
||||
const freshAfter = await providersDb.getProviderConnectionById(fresh.id);
|
||||
const staleAfter = await providersDb.getProviderConnectionById((stale as any).id);
|
||||
const freshAfter = await providersDb.getProviderConnectionById((fresh as any).id);
|
||||
|
||||
assert.equal(Array.isArray(postedBody.providers), true);
|
||||
assert.equal(Array.isArray(postedBody.apiKeys), true);
|
||||
|
||||
@@ -47,7 +47,7 @@ async function readJsonFileWithRetry(filePath, attempts = 100) {
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
const snapshots = trimmed
|
||||
.split(/\n(?=\{)/)
|
||||
.map((entry) => entry.trim())
|
||||
@@ -69,7 +69,7 @@ async function readJsonFileWithRetry(filePath, attempts = 100) {
|
||||
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||||
try {
|
||||
return parseJsonSnapshot(await fs.readFile(filePath, "utf8"));
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
@@ -101,7 +101,7 @@ test.afterEach(async () => {
|
||||
restoreEnv();
|
||||
|
||||
for (const dir of tempDirs) {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
await fs.rm(dir as any, { recursive: true, force: true });
|
||||
}
|
||||
tempDirs.clear();
|
||||
});
|
||||
|
||||
@@ -69,20 +69,20 @@ test("migration backfills Codex request defaults, preserves existing providerSpe
|
||||
|
||||
assert.equal(firstRun.migrated, true);
|
||||
assert.deepEqual(firstRun.updatedConnectionIds.sort(), [first.id, second.id].sort());
|
||||
assert.deepEqual(byId.get(first.id).providerSpecificData.requestDefaults, {
|
||||
assert.deepEqual((byId.get(first.id).providerSpecificData as any).requestDefaults, {
|
||||
reasoningEffort: "medium",
|
||||
serviceTier: "priority",
|
||||
});
|
||||
assert.deepEqual(byId.get(second.id).providerSpecificData.requestDefaults, {
|
||||
assert.deepEqual((byId.get(second.id) as any).providerSpecificData.requestDefaults, {
|
||||
reasoningEffort: "high",
|
||||
serviceTier: "priority",
|
||||
});
|
||||
assert.deepEqual(byId.get(untouched.id).providerSpecificData.requestDefaults, {
|
||||
assert.deepEqual((byId.get(untouched.id) as any).providerSpecificData.requestDefaults, {
|
||||
reasoningEffort: "low",
|
||||
serviceTier: "priority",
|
||||
});
|
||||
assert.equal(byId.get(first.id).providerSpecificData.tag, "team-a");
|
||||
assert.deepEqual(byId.get(first.id).providerSpecificData.codexLimitPolicy, {
|
||||
assert.equal((byId.get((first as any).id).providerSpecificData as any).tag, "team-a");
|
||||
assert.deepEqual((byId as any).get(first.id).providerSpecificData.codexLimitPolicy, {
|
||||
use5h: false,
|
||||
useWeekly: true,
|
||||
});
|
||||
@@ -110,26 +110,26 @@ test("provider connection persistence normalizes request defaults without droppi
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(created.providerSpecificData.requestDefaults, {
|
||||
(assert as any).deepEqual((created.providerSpecificData as any).requestDefaults, {
|
||||
reasoningEffort: "high",
|
||||
serviceTier: "priority",
|
||||
customFlag: "keep-me",
|
||||
});
|
||||
assert.equal(created.providerSpecificData.openaiStoreEnabled, true);
|
||||
assert.equal(created.providerSpecificData.workspaceId, "ws-normalize");
|
||||
assert.equal(created.providerSpecificData.tag, "team-z");
|
||||
assert.equal((created.providerSpecificData as any).openaiStoreEnabled, true);
|
||||
assert.equal((created.providerSpecificData as any).workspaceId, "ws-normalize");
|
||||
assert.equal((created.providerSpecificData as any).tag, "team-z");
|
||||
|
||||
const updated = await providersDb.updateProviderConnection(created.id, {
|
||||
const updated = await providersDb.updateProviderConnection((created as any).id, {
|
||||
providerSpecificData: {
|
||||
...created.providerSpecificData,
|
||||
requestDefaults: { reasoningEffort: "medium" },
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(updated.providerSpecificData.requestDefaults, {
|
||||
assert.deepEqual((updated.providerSpecificData as any).requestDefaults, {
|
||||
reasoningEffort: "medium",
|
||||
});
|
||||
assert.equal(updated.providerSpecificData.openaiStoreEnabled, true);
|
||||
assert.equal(updated.providerSpecificData.workspaceId, "ws-normalize");
|
||||
assert.equal(updated.providerSpecificData.tag, "team-z");
|
||||
assert.equal((updated.providerSpecificData as any).openaiStoreEnabled, true);
|
||||
assert.equal((updated.providerSpecificData as any).workspaceId, "ws-normalize");
|
||||
assert.equal((updated.providerSpecificData as any).tag, "team-z");
|
||||
});
|
||||
|
||||
@@ -227,7 +227,7 @@ test("chatCore converts Responses-style SSE fallback into JSON when stream=false
|
||||
responseFactory: () => buildResponsesSse("Brasilia"),
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(call.headers.Accept || call.headers.accept, "application/json");
|
||||
@@ -250,7 +250,7 @@ test("chatCore converts Responses-style NDJSON fallback into JSON when stream=fa
|
||||
responseFactory: () => buildResponsesNdjson("Brasilia"),
|
||||
});
|
||||
|
||||
const payload = await result.response.json();
|
||||
const payload = (await result.response.json()) as any;
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(call.headers.Accept || call.headers.accept, "application/json");
|
||||
@@ -300,7 +300,7 @@ test("handleComboChat validates non-stream quality using the original client str
|
||||
allCombos: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(seenModels, ["codex/gpt-5.4", "openai/gpt-4o-mini"]);
|
||||
|
||||
@@ -125,7 +125,7 @@ test("combo builder options route aggregates providers, connections, models and
|
||||
});
|
||||
|
||||
const response = await route.GET();
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.schemaVersion, 2);
|
||||
@@ -212,7 +212,7 @@ test("combo builder options route exposes compatible provider nodes with node me
|
||||
await modelsDb.addCustomModel("openai-compatible-demo", "gpt-custom", "GPT Custom");
|
||||
|
||||
const response = await route.GET();
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
const provider = body.providers.find((entry) => entry.providerId === "openai-compatible-demo");
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
@@ -113,7 +113,7 @@ test("combo health route exposes step-level target health for structured combos"
|
||||
const response = await route.GET(
|
||||
new Request(`http://localhost/api/usage/combo-health?range=24h&comboId=${combo.id}`)
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.combos.length, 1);
|
||||
@@ -234,7 +234,7 @@ test("combo health route prefers historical call log target metrics over volatil
|
||||
const response = await route.GET(
|
||||
new Request(`http://localhost/api/usage/combo-health?range=24h&comboId=${combo.id}`)
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.combos.length, 1);
|
||||
|
||||
@@ -91,7 +91,7 @@ test("combo failover skips the cooled provider target on the next request", asyn
|
||||
},
|
||||
})
|
||||
);
|
||||
const firstBody = await firstResponse.json();
|
||||
const firstBody = (await firstResponse.json()) as any;
|
||||
|
||||
const secondResponse = await handleChat(
|
||||
buildRequest({
|
||||
@@ -102,7 +102,7 @@ test("combo failover skips the cooled provider target on the next request", asyn
|
||||
},
|
||||
})
|
||||
);
|
||||
const secondBody = await secondResponse.json();
|
||||
const secondBody = (await secondResponse.json()) as any;
|
||||
|
||||
assert.equal(firstResponse.status, 200);
|
||||
assert.equal(secondResponse.status, 200);
|
||||
|
||||
@@ -96,7 +96,7 @@ test("POST /api/combos rejects composite tiers that point to unknown steps", asy
|
||||
},
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.deepEqual(body, {
|
||||
@@ -114,12 +114,12 @@ test("POST /api/combos rejects composite tiers that point to unknown steps", asy
|
||||
|
||||
test("POST /api/combos persists valid composite tiers", async () => {
|
||||
const response = await createRoute.POST(makeCreateRequest(createTieredComboInput()));
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
const stored = await combosDb.getComboByName("tiered-codex");
|
||||
|
||||
assert.equal(response.status, 201);
|
||||
assert.equal(body.config.compositeTiers.defaultTier, "primary");
|
||||
assert.equal(stored.config.compositeTiers.tiers.primary.stepId, "step-primary");
|
||||
assert.equal((stored.config as any).compositeTiers.tiers.primary.stepId, "step-primary");
|
||||
assert.equal(stored.models[0].id, "step-primary");
|
||||
});
|
||||
|
||||
@@ -137,7 +137,7 @@ test("POST /api/combos preserves legacy string combo refs during normalization",
|
||||
models: ["child-ref"],
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
const stored = await combosDb.getComboByName("parent-ref");
|
||||
|
||||
assert.equal(response.status, 201);
|
||||
@@ -164,7 +164,7 @@ test("PUT /api/combos rejects updates that orphan an existing composite tier ste
|
||||
}),
|
||||
{ params: Promise.resolve({ id: combo.id }) }
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.deepEqual(body, {
|
||||
@@ -198,8 +198,8 @@ test("PUT /api/combos preserves legacy string combo refs during normalization",
|
||||
}),
|
||||
{ params: Promise.resolve({ id: combo.id }) }
|
||||
);
|
||||
const body = await response.json();
|
||||
const stored = await combosDb.getComboById(combo.id);
|
||||
const body = (await response.json()) as any;
|
||||
const stored = await combosDb.getComboById((combo as any).id);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.models[0].kind, "combo-ref");
|
||||
|
||||
@@ -113,7 +113,7 @@ async function cleanupTestDataDir() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
return;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
@@ -622,7 +622,7 @@ test("handleComboChat preserves the first failure status but surfaces the last e
|
||||
relayOptions: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
|
||||
assert.equal(result.status, 500);
|
||||
assert.equal(payload.error.message, "fail:model-b");
|
||||
@@ -982,7 +982,7 @@ test("handleComboChat returns the earliest retry-after when all priority targets
|
||||
relayOptions: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
|
||||
assert.equal(result.status, 429);
|
||||
assert.match(payload.error.message, /limited:model-b/);
|
||||
@@ -1013,7 +1013,7 @@ test("handleComboChat returns 404 model_not_found when a combo has no executable
|
||||
relayOptions: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
|
||||
assert.equal(result.status, 404);
|
||||
assert.equal(payload.error.code, "model_not_found");
|
||||
@@ -1046,7 +1046,7 @@ test("handleComboChat round-robin returns 404 when no models are configured", as
|
||||
relayOptions: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
|
||||
assert.equal(result.status, 404);
|
||||
assert.equal(payload.error.code, "model_not_found");
|
||||
@@ -1137,7 +1137,7 @@ test("handleComboChat round-robin surfaces retry-after metadata after exhausting
|
||||
relayOptions: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
|
||||
assert.equal(result.status, 429);
|
||||
assert.match(payload.error.message, /rr-limited:model-b/);
|
||||
@@ -1222,7 +1222,7 @@ test("handleComboChat round-robin falls through provider-scoped 400s and returns
|
||||
relayOptions: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
assert.equal(result.status, 400);
|
||||
assert.equal(payload.error.message, "rr-final-fail");
|
||||
assert.deepEqual(calls, ["model-a", "model-b"]);
|
||||
@@ -1388,7 +1388,7 @@ test("handleComboChat returns a 503 when every model is unavailable before execu
|
||||
relayOptions: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
assert.equal(result.status, 503);
|
||||
assert.equal(payload.error.code, "ALL_ACCOUNTS_INACTIVE");
|
||||
});
|
||||
@@ -1694,7 +1694,7 @@ test("handleComboChat context cache protection pins the model and tags tool-call
|
||||
relayOptions: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(calls, ["claude/claude-sonnet-4-6"]);
|
||||
assert.match(
|
||||
@@ -1809,7 +1809,7 @@ test("handleComboChat round-robin resolves nested combos and returns inactive wh
|
||||
],
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
assert.equal(result.status, 503);
|
||||
assert.equal(payload.error.code, "ALL_ACCOUNTS_INACTIVE");
|
||||
});
|
||||
@@ -1947,7 +1947,7 @@ test("handleComboChat falls back to next model when first model returns all-acco
|
||||
relayOptions: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
// First model returns 503 with "unavailable" → combo should try model-b next
|
||||
// If the fix is not applied, combo would abort here and return 503 immediately
|
||||
assert.equal(result.ok, true);
|
||||
@@ -1988,7 +1988,7 @@ test("handleComboChat round-robin falls back when all-accounts-rate-limited 503
|
||||
relayOptions: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(calls, ["model-a", "model-b"]);
|
||||
assert.equal(payload.choices[0].message.content, "ok");
|
||||
@@ -2022,7 +2022,7 @@ test("handleComboChat aborts combo when 503 response does NOT contain the unavai
|
||||
relayOptions: null,
|
||||
});
|
||||
|
||||
const payload = await result.json();
|
||||
const payload = (await result.json()) as any;
|
||||
// Without the fix, combo would abort (still 503). With the fix, it's still 503 because
|
||||
// the signal check filters out non-JSON or non-"unavailable" responses.
|
||||
assert.equal(result.status, 503);
|
||||
|
||||
@@ -48,7 +48,7 @@ test("handleComboChat with 'usage' strategy hits sortModelsByUsage", async () =>
|
||||
try {
|
||||
const res = await handleComboChat(id, req, stubConnection());
|
||||
assert.equal(res.status >= 200, true);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
// Expect error as fetch is not globally mocked for this quick edge branch test, that's fine
|
||||
}
|
||||
});
|
||||
@@ -70,7 +70,7 @@ test("handleComboChat with 'context' strategy hits sortModelsByContextSize", asy
|
||||
|
||||
try {
|
||||
const res = await handleComboChat(id, req, stubConnection());
|
||||
} catch (e) {}
|
||||
} catch (e: any) {}
|
||||
});
|
||||
|
||||
test("handleComboChat hits extractPromptForIntent edge cases", async () => {
|
||||
@@ -99,8 +99,8 @@ test("handleComboChat hits extractPromptForIntent edge cases", async () => {
|
||||
|
||||
try {
|
||||
await handleComboChat(id, reqNull, stubConnection());
|
||||
} catch (e) {}
|
||||
} catch (e: any) {}
|
||||
try {
|
||||
await handleComboChat(id, reqEmpty, stubConnection());
|
||||
} catch (e) {}
|
||||
} catch (e: any) {}
|
||||
});
|
||||
|
||||
@@ -74,12 +74,12 @@ test("combo test route validates request payloads and combo existence", async ()
|
||||
body: JSON.stringify({ comboName: "" }),
|
||||
})
|
||||
);
|
||||
const invalidBody = await invalidBodyResponse.json();
|
||||
const invalidBody = (await invalidBodyResponse.json()) as any;
|
||||
assert.equal(invalidBodyResponse.status, 400);
|
||||
assert.equal(invalidBody.error.message, "Invalid request");
|
||||
|
||||
const missingResponse = await route.POST(makeRequest("missing-combo"));
|
||||
const missingBody = await missingResponse.json();
|
||||
const missingBody = (await missingResponse.json()) as any;
|
||||
assert.equal(missingResponse.status, 404);
|
||||
assert.equal(missingBody.error, "Combo not found");
|
||||
});
|
||||
@@ -120,7 +120,7 @@ test("combo test route marks a model healthy only when it returns assistant text
|
||||
} finally {
|
||||
Math.random = originalRandom;
|
||||
}
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
const forwardedBody = JSON.parse(fetchCalls[0].init.body);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
@@ -163,7 +163,7 @@ test("combo test route treats empty successful responses as failures", async ()
|
||||
);
|
||||
|
||||
const response = await route.POST(makeRequest());
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.resolvedBy, null);
|
||||
@@ -203,7 +203,7 @@ test("combo test route accepts reasoning-only completions as healthy smoke-test
|
||||
);
|
||||
|
||||
const response = await route.POST(makeRequest());
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.resolvedBy, "openrouter/openai/gpt-5.4");
|
||||
@@ -228,7 +228,7 @@ test("combo test route surfaces provider errors instead of downgrading them to r
|
||||
);
|
||||
|
||||
const response = await route.POST(makeRequest());
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.resolvedBy, null);
|
||||
@@ -284,7 +284,7 @@ test("combo test route launches model probes concurrently while preserving combo
|
||||
);
|
||||
|
||||
const response = await responsePromise;
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.resolvedBy, "provider/first");
|
||||
@@ -343,7 +343,7 @@ test("combo test route preserves structured step metadata for repeated model/acc
|
||||
};
|
||||
|
||||
const response = await route.POST(makeRequest());
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
@@ -366,7 +366,7 @@ test("combo test route rejects empty combos and respects forwarded base URLs", a
|
||||
await createTestCombo([]);
|
||||
|
||||
const emptyResponse = await route.POST(makeRequest());
|
||||
const emptyBody = await emptyResponse.json();
|
||||
const emptyBody = (await emptyResponse.json()) as any;
|
||||
assert.equal(emptyResponse.status, 400);
|
||||
assert.equal(emptyBody.error, "Combo has no models");
|
||||
|
||||
@@ -422,7 +422,7 @@ test("combo test route handles upstream timeouts and non-JSON error bodies", asy
|
||||
};
|
||||
|
||||
const response = await route.POST(makeRequest());
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.resolvedBy, null);
|
||||
|
||||
@@ -60,7 +60,7 @@ test("compliance audit route keeps array payloads and exposes total count with s
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.headers.get("x-total-count"), "1");
|
||||
assert.equal(response.headers.get("x-page-limit"), "10");
|
||||
const payload = await response.json();
|
||||
const payload = (await response.json()) as any;
|
||||
assert.equal(Array.isArray(payload), true);
|
||||
assert.equal(payload.length, 1);
|
||||
assert.equal(payload[0].action, "provider.validation.ssrf_blocked");
|
||||
|
||||
@@ -233,13 +233,14 @@ test("cleanupExpiredLogs removes stale rows across all log tables and records an
|
||||
);
|
||||
|
||||
const result = compliance.cleanupExpiredLogs();
|
||||
const usageCount = db.prepare("SELECT COUNT(*) as count FROM usage_history").get().count;
|
||||
const callCount = db.prepare("SELECT COUNT(*) as count FROM call_logs").get().count;
|
||||
const proxyCount = db.prepare("SELECT COUNT(*) as count FROM proxy_logs").get().count;
|
||||
const requestDetailCount = db
|
||||
.prepare("SELECT COUNT(*) as count FROM request_detail_logs")
|
||||
.get().count;
|
||||
const mcpAuditCount = db.prepare("SELECT COUNT(*) as count FROM mcp_tool_audit").get().count;
|
||||
const usageCount = (db.prepare("SELECT COUNT(*) as count FROM usage_history").get() as any).count;
|
||||
const callCount = (db.prepare("SELECT COUNT(*) as count FROM call_logs").get() as any).count;
|
||||
const proxyCount = (db.prepare("SELECT COUNT(*) as count FROM proxy_logs").get() as any).count;
|
||||
const requestDetailCount = (
|
||||
db.prepare("SELECT COUNT(*) as count FROM request_detail_logs") as any
|
||||
).get().count;
|
||||
const mcpAuditCount = (db.prepare("SELECT COUNT(*) as count FROM mcp_tool_audit").get() as any)
|
||||
.count;
|
||||
const auditEntries = compliance.getAuditLog();
|
||||
const auditActions = auditEntries.map((entry) => entry.action);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ test("console log API normalizes numeric pino levels correctly", async () => {
|
||||
const response = await route.GET(
|
||||
new Request("http://localhost/api/logs/console?level=info&limit=10")
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(
|
||||
@@ -89,7 +89,7 @@ test("console log API filters by component, time window, and result limit", asyn
|
||||
const response = await route.GET(
|
||||
new Request("http://localhost/api/logs/console?level=warn&component=router&limit=1")
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.length, 1);
|
||||
@@ -111,7 +111,7 @@ test("console log API returns an empty list for a missing file and surfaces read
|
||||
try {
|
||||
const brokenResponse = await route.GET(new Request("http://localhost/api/logs/console"));
|
||||
assert.equal(brokenResponse.status, 500);
|
||||
const payload = await brokenResponse.json();
|
||||
const payload = (await brokenResponse.json()) as any;
|
||||
assert.equal(typeof payload.error, "string");
|
||||
assert.equal(payload.error.length > 0, true);
|
||||
} finally {
|
||||
|
||||
@@ -82,7 +82,7 @@ test("compressContext: Layer 1 — trims long tool messages", () => {
|
||||
// Use target limit that allows the truncated tool message (~1000 tokens) to survive
|
||||
const result = compressContext(body, { maxTokens: 2000, reserveTokens: 100 });
|
||||
assert.ok(result.compressed);
|
||||
const toolMsg = result.body.messages.find((m: any) => m.role === "tool");
|
||||
const toolMsg = (result.body.messages as any).find((m: any) => m.role === "tool");
|
||||
assert.ok(toolMsg.content.length < longContent.length);
|
||||
assert.ok(toolMsg.content.includes("[truncated]"));
|
||||
});
|
||||
@@ -111,7 +111,7 @@ test("compressContext: Layer 2 — compresses thinking in old messages", () => {
|
||||
};
|
||||
const result = compressContext(body, { maxTokens: 2000, reserveTokens: 500 });
|
||||
// First assistant should have thinking removed
|
||||
const firstAssistant = result.body.messages.find((m: any) => m.role === "assistant");
|
||||
const firstAssistant = (result.body as any).messages.find((m: any) => m.role === "assistant");
|
||||
if (Array.isArray(firstAssistant.content)) {
|
||||
const hasThinking = firstAssistant.content.some((b: any) => b.type === "thinking");
|
||||
assert.equal(hasThinking, false);
|
||||
@@ -129,7 +129,7 @@ test("compressContext: Layer 3 — drops old messages to fit", () => {
|
||||
const body = { model: "test", messages };
|
||||
const result = compressContext(body, { maxTokens: 3000, reserveTokens: 500 });
|
||||
assert.ok(result.compressed);
|
||||
assert.ok(result.body.messages.length < messages.length);
|
||||
assert.ok((result as any).body.messages.length < messages.length);
|
||||
assert.equal(result.body.messages[0].role, "system");
|
||||
});
|
||||
|
||||
@@ -243,7 +243,7 @@ test("Layer 3: preserves intact tool_use/tool_result pairs after compression", (
|
||||
];
|
||||
const body = { model: "test", messages };
|
||||
const result = compressContext(body, { maxTokens: 50000, reserveTokens: 10000 });
|
||||
const toolMsg = result.body.messages.find(
|
||||
const toolMsg = (result.body.messages as any).find(
|
||||
(m: any) => m.role === "tool" && m.tool_call_id === "call_1"
|
||||
);
|
||||
assert.ok(toolMsg, "tool_result for call_1 should survive compression");
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("Context pinning — tool call responses (#721)", () => {
|
||||
assert.equal(result.length, 3, "Should have 3 messages (original 2 + synthetic)");
|
||||
assert.equal(result[2].role, "assistant");
|
||||
assert.ok(
|
||||
result[2].content.includes("<omniModel>ollamacloud/glm-5</omniModel>"),
|
||||
(result[2].content as any).includes("<omniModel>ollamacloud/glm-5</omniModel>"),
|
||||
"Synthetic message should contain the pin tag"
|
||||
);
|
||||
});
|
||||
@@ -50,7 +50,7 @@ describe("Context pinning — tool call responses (#721)", () => {
|
||||
// Array content → should append synthetic message
|
||||
assert.equal(result.length, 3);
|
||||
assert.equal(result[2].role, "assistant");
|
||||
assert.ok(result[2].content.includes("<omniModel>nvidia/llama-3.4-70b</omniModel>"));
|
||||
assert.ok((result[2] as any).content.includes("<omniModel>nvidia/llama-3.4-70b</omniModel>"));
|
||||
});
|
||||
|
||||
test("extractPinnedModel finds tag in synthetic message after tool_calls", () => {
|
||||
@@ -79,8 +79,8 @@ describe("Context pinning — tool call responses (#721)", () => {
|
||||
const result = injectModelTag(messages, "openai/gpt-4o");
|
||||
|
||||
assert.equal(result.length, 2, "Should not add a new message");
|
||||
assert.ok(result[1].content.includes("<omniModel>openai/gpt-4o</omniModel>"));
|
||||
assert.ok(result[1].content.startsWith("Hi there!"));
|
||||
assert.ok((result as any)[1].content.includes("<omniModel>openai/gpt-4o</omniModel>"));
|
||||
(assert as any).ok((result[1].content as any).startsWith("Hi there!"));
|
||||
});
|
||||
|
||||
test("roundtrip: inject → extract works for tool-call messages", () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ async function resetStorage() {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
}
|
||||
break;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if ((error?.code === "EBUSY" || error?.code === "EPERM") && attempt < 9) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
||||
} else {
|
||||
|
||||
@@ -126,7 +126,7 @@ test("restoreDbBackup restores SQLite contents and returns entity counts", async
|
||||
assert.equal(restored.nodeCount, 0);
|
||||
assert.equal(restored.comboCount, 0);
|
||||
assert.equal(restored.apiKeyCount, 0);
|
||||
assert.equal(row.cnt, 1);
|
||||
assert.equal((row as any).cnt, 1);
|
||||
});
|
||||
|
||||
test("cleanupDbBackups removes overflow families and orphaned sidecars", async () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ async function resetStorage() {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
}
|
||||
break;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if ((error?.code === "EBUSY" || error?.code === "EPERM") && attempt < 9) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
||||
} else {
|
||||
@@ -58,7 +58,7 @@ test("createCombo stores default strategy and supports lookup by id and name", a
|
||||
weight: 0,
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(await combosDb.getComboById(combo.id), combo);
|
||||
assert.deepEqual(await combosDb.getComboById((combo as any).id), combo);
|
||||
assert.deepEqual(await combosDb.getComboByName("Priority Combo"), combo);
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ test("updateCombo merges fields while preserving immutable data", async () => {
|
||||
config: { retries: 1 },
|
||||
});
|
||||
|
||||
const updated = await combosDb.updateCombo(combo.id, {
|
||||
const updated = await combosDb.updateCombo((combo as any).id, {
|
||||
strategy: "round-robin",
|
||||
config: { retries: 3, timeoutMs: 2000 },
|
||||
isHidden: true,
|
||||
@@ -103,7 +103,7 @@ test("updateCombo merges fields while preserving immutable data", async () => {
|
||||
assert.deepEqual(updated.config, { retries: 3, timeoutMs: 2000 });
|
||||
assert.equal(updated.strategy, "round-robin");
|
||||
assert.equal(updated.isHidden, true);
|
||||
assert.deepEqual(await combosDb.getComboById(combo.id), updated);
|
||||
assert.deepEqual(await combosDb.getComboById((combo as any).id), updated);
|
||||
});
|
||||
|
||||
test("reorderCombos persists manual combo ordering in sqlite", async () => {
|
||||
@@ -130,9 +130,9 @@ test("reorderCombos persists manual combo ordering in sqlite", async () => {
|
||||
reordered.map((combo) => combo.sortOrder),
|
||||
[1, 2, 3]
|
||||
);
|
||||
assert.equal((await combosDb.getComboById(charlie.id))?.sortOrder, 1);
|
||||
assert.equal((await combosDb.getComboById(alpha.id))?.sortOrder, 2);
|
||||
assert.equal((await combosDb.getComboById(bravo.id))?.sortOrder, 3);
|
||||
assert.equal((await combosDb.getComboById((charlie as any).id))?.sortOrder, 1);
|
||||
assert.equal((await combosDb.getComboById((alpha as any).id))?.sortOrder, 2);
|
||||
assert.equal((await combosDb.getComboById((bravo as any).id))?.sortOrder, 3);
|
||||
});
|
||||
|
||||
test("deleteCombo reports missing ids and removes existing rows", async () => {
|
||||
@@ -142,8 +142,8 @@ test("deleteCombo reports missing ids and removes existing rows", async () => {
|
||||
});
|
||||
|
||||
assert.equal(await combosDb.deleteCombo("missing-combo"), false);
|
||||
assert.equal(await combosDb.deleteCombo(combo.id), true);
|
||||
assert.equal(await combosDb.getComboById(combo.id), null);
|
||||
assert.equal(await combosDb.deleteCombo((combo as any).id), true);
|
||||
assert.equal(await combosDb.getComboById((combo as any).id), null);
|
||||
});
|
||||
|
||||
test("getCombos upgrades legacy persisted entries to version 2 and resolves combo refs", async () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user