fix(build): resolve all TypeScript compilation errors and Next.js 15 dynamic route slug conflicts

- Fix Next.js 15 async params in 4 API route handlers (accounts, providers, registered-keys)
- Move providers/[id]/limits → providers/[provider]/limits to resolve slug name conflict
- Add keytar to serverExternalPackages and KNOWN_EXTERNALS in next.config.mjs
- Fix Zod z.record() arity across a2a.ts and issues/report/route.ts
- Fix SearchResponse interface (optional answer property) in SearchTools and ResultsPanel
- Fix ProviderLimits implicit any types in index.tsx and utils.tsx
- Fix better-sqlite3 prepare<T> generic usage in secrets.ts
- Remove duplicate pricing keys (gemini-3-flash-preview)
- Cast analytics result, ApiErrorType import, TaskRoutingConfig type
- Remove rogue app/ duplicate directory from project root

Resolves: #560
This commit is contained in:
diegosouzapw
2026-03-23 18:23:08 -03:00
parent 428fa9404c
commit 92e0f242c7
19 changed files with 129 additions and 114 deletions

View File

@@ -17,6 +17,7 @@ const nextConfig = {
"pino-pretty",
"thread-stream",
"better-sqlite3",
"keytar",
"zod",
"child_process",
"fs",
@@ -70,6 +71,7 @@ const nextConfig = {
const KNOWN_EXTERNALS = new Set([
"better-sqlite3",
"keytar",
"zod",
"pino",
"pino-pretty",

View File

@@ -57,7 +57,7 @@ export const TaskInputSchema = z.object({
role: z
.enum(["coding", "review", "planning", "analysis", "debugging", "documentation"])
.optional(),
metadata: z.record(z.unknown()).optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
});
export const CostEnvelopeSchema = z.object({
@@ -120,7 +120,7 @@ export type PolicyVerdict = z.infer<typeof PolicyVerdictSchema>;
export const JsonRpcRequestSchema = z.object({
jsonrpc: z.literal("2.0"),
method: z.enum(["message/send", "message/stream", "tasks/get", "tasks/cancel"]),
params: z.record(z.unknown()),
params: z.record(z.string(), z.unknown()),
id: z.union([z.string(), z.number()]),
});
@@ -151,7 +151,7 @@ export const MessageSendParamsSchema = z.object({
message: z.object({
role: z.string().default("user"),
content: z.string(),
metadata: z.record(z.unknown()).optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
}),
config: z
.object({

View File

@@ -39,6 +39,7 @@ interface SearchResponse {
id: string;
provider: string;
query: string;
answer?: string;
results: SearchResult[];
cached: boolean;
usage: {

View File

@@ -20,7 +20,7 @@ interface SearchResponse {
provider: string;
results: SearchResult[];
query: string;
answer: string | null;
answer?: string | null;
cached: boolean;
usage: {
queries_used: number;

View File

@@ -138,67 +138,70 @@ export default function ProviderLimits() {
}
}, []);
const fetchQuota = useCallback(async (connectionId, provider, options = {}) => {
const force = options?.force === true;
// Debounce: skip if last fetch was < MIN_FETCH_INTERVAL_MS ago
const now = Date.now();
const lastFetch = lastFetchTimeRef.current[connectionId] || 0;
if (!force && now - lastFetch < MIN_FETCH_INTERVAL_MS) {
return; // Skip, data is still fresh
}
lastFetchTimeRef.current[connectionId] = now;
setLoading((prev) => ({ ...prev, [connectionId]: true }));
setErrors((prev) => ({ ...prev, [connectionId]: null }));
try {
const response = await fetch(`/api/usage/${connectionId}`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMsg = errorData.error || response.statusText;
if (response.status === 404) return;
if (response.status === 401) {
setQuotaData((prev) => ({
...prev,
[connectionId]: { quotas: [], message: errorMsg },
}));
return;
}
throw new Error(`HTTP ${response.status}: ${errorMsg}`);
const fetchQuota = useCallback(
async (connectionId, provider, options: { force?: boolean } = {}) => {
const force = options?.force === true;
// Debounce: skip if last fetch was < MIN_FETCH_INTERVAL_MS ago
const now = Date.now();
const lastFetch = lastFetchTimeRef.current[connectionId] || 0;
if (!force && now - lastFetch < MIN_FETCH_INTERVAL_MS) {
return; // Skip, data is still fresh
}
const data = await response.json();
const parsedQuotas = parseQuotaData(provider, data);
lastFetchTimeRef.current[connectionId] = now;
// T13: If resetAt already passed but provider still returned stale cumulative usage,
// display 0 immediately and trigger a background probe to refresh snapshot.
const hasStaleAfterReset = parsedQuotas.some((q) => q?.staleAfterReset === true);
if (hasStaleAfterReset) {
const lastProbeAt = staleProbeRef.current[connectionId] || 0;
if (Date.now() - lastProbeAt >= MIN_FETCH_INTERVAL_MS) {
staleProbeRef.current[connectionId] = Date.now();
setTimeout(() => {
fetchQuota(connectionId, provider, { force: true }).catch(() => {});
}, 5000);
setLoading((prev) => ({ ...prev, [connectionId]: true }));
setErrors((prev) => ({ ...prev, [connectionId]: null }));
try {
const response = await fetch(`/api/usage/${connectionId}`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMsg = errorData.error || response.statusText;
if (response.status === 404) return;
if (response.status === 401) {
setQuotaData((prev) => ({
...prev,
[connectionId]: { quotas: [], message: errorMsg },
}));
return;
}
throw new Error(`HTTP ${response.status}: ${errorMsg}`);
}
}
const data = await response.json();
const parsedQuotas = parseQuotaData(provider, data);
setQuotaData((prev) => ({
...prev,
[connectionId]: {
quotas: parsedQuotas,
plan: data.plan || null,
message: data.message || null,
raw: data,
},
}));
} catch (error) {
setErrors((prev) => ({
...prev,
[connectionId]: error.message || "Failed to fetch quota",
}));
} finally {
setLoading((prev) => ({ ...prev, [connectionId]: false }));
}
}, []);
// T13: If resetAt already passed but provider still returned stale cumulative usage,
// display 0 immediately and trigger a background probe to refresh snapshot.
const hasStaleAfterReset = parsedQuotas.some((q) => q?.staleAfterReset === true);
if (hasStaleAfterReset) {
const lastProbeAt = staleProbeRef.current[connectionId] || 0;
if (Date.now() - lastProbeAt >= MIN_FETCH_INTERVAL_MS) {
staleProbeRef.current[connectionId] = Date.now();
setTimeout(() => {
fetchQuota(connectionId, provider, { force: true }).catch(() => {});
}, 5000);
}
}
setQuotaData((prev) => ({
...prev,
[connectionId]: {
quotas: parsedQuotas,
plan: data.plan || null,
message: data.message || null,
raw: data,
},
}));
} catch (error) {
setErrors((prev) => ({
...prev,
[connectionId]: error.message || "Failed to fetch quota",
}));
} finally {
setLoading((prev) => ({ ...prev, [connectionId]: false }));
}
},
[]
);
const refreshProvider = useCallback(
async (connectionId, provider) => {

View File

@@ -84,7 +84,7 @@ function isPastResetWindow(resetAt) {
return Date.now() >= resetTime;
}
function normalizeQuotaEntry(name, quota = {}, extras = {}) {
function normalizeQuotaEntry(name: string, quota: any = {}, extras: any = {}) {
const usedRaw = Number(quota?.used || 0);
const totalRaw = Number(quota?.total || 0);
const resetAt = quota?.resetAt || null;

View File

@@ -1,4 +1,4 @@
import { NextResponse, type Request } from "next/server";
import { NextResponse } from "next/server";
import { getSettings, updateSettings } from "@/lib/localDb";
import { setDefaultFastServiceTierEnabled } from "@omniroute/open-sse/executors/codex.ts";
import { updateCodexServiceTierSchema } from "@/shared/validation/schemas";
@@ -35,7 +35,7 @@ export async function PUT(request: Request) {
},
{ status: 400 }
);
}
}
try {
const validation = validateBody(updateCodexServiceTierSchema, rawBody);

View File

@@ -8,7 +8,11 @@ import {
import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher";
import { updateProxyConfigSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
import {
createErrorResponse,
createErrorResponseFromUnknown,
type ApiErrorType,
} from "@/lib/api/errorResponse";
import type { z } from "zod";
const BASE_SUPPORTED_PROXY_TYPES = new Set(["http", "https"]);
@@ -174,7 +178,8 @@ export async function PUT(request: Request) {
} catch (error) {
const routeError = toApiRouteError(error);
const status = Number(routeError.status) || 500;
const type = routeError.type || (status === 400 ? "invalid_request" : "server_error");
const type = (routeError.type ||
(status === 400 ? "invalid_request" : "server_error")) as ApiErrorType;
return createErrorResponse({ status, message: routeError.message, type });
}
}

View File

@@ -51,7 +51,8 @@ export async function PUT(request: Request) {
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const config = validation.data;
const config =
validation.data as import("@omniroute/open-sse/services/taskAwareRouter.ts").TaskRoutingConfig;
setTaskRoutingConfig(config);

View File

@@ -64,7 +64,7 @@ export async function GET(request) {
/* ignore */
}
const analytics = await computeAnalytics(history, range, connectionMap);
const analytics: any = await computeAnalytics(history, range, connectionMap);
// T01: fallback transparency metrics from call_logs (requested_model vs routed model).
try {

View File

@@ -13,20 +13,21 @@ const limitsSchema = z.object({
* GET /api/v1/accounts/[id]/limits
* Get the current issuance limits for an account.
*/
export async function GET(request: Request, { params }: { params: { id: string } }) {
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
}
const limits = getAccountKeyLimit(params.id);
return NextResponse.json({ accountId: params.id, limits: limits ?? null });
const resolvedParams = await params;
const limits = getAccountKeyLimit(resolvedParams.id);
return NextResponse.json({ accountId: resolvedParams.id, limits: limits ?? null });
}
/**
* PUT /api/v1/accounts/[id]/limits
* Configure issuance limits for an account.
*/
export async function PUT(request: Request, { params }: { params: { id: string } }) {
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
}
@@ -43,7 +44,8 @@ export async function PUT(request: Request, { params }: { params: { id: string }
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
}
setAccountKeyLimit(params.id, parsed.data);
const updated = getAccountKeyLimit(params.id);
return NextResponse.json({ accountId: params.id, limits: updated });
const resolvedParams = await params;
setAccountKeyLimit(resolvedParams.id, parsed.data);
const updated = getAccountKeyLimit(resolvedParams.id);
return NextResponse.json({ accountId: resolvedParams.id, limits: updated });
}

View File

@@ -65,7 +65,7 @@ export async function POST(request) {
let dynamicProviders: ReturnType<typeof buildDynamicAudioProvider>[] = [];
try {
const nodes = await getProviderNodes();
dynamicProviders = (Array.isArray(nodes) ? nodes : [])
dynamicProviders = (Array.isArray(nodes) ? (nodes as unknown as ProviderNodeRow[]) : [])
.filter((n: ProviderNodeRow) => {
if (n.apiType !== "chat" && n.apiType !== "responses") return false;
try {

View File

@@ -63,7 +63,7 @@ export async function POST(request) {
let dynamicProviders: ReturnType<typeof buildDynamicAudioProvider>[] = [];
try {
const nodes = await getProviderNodes();
dynamicProviders = (Array.isArray(nodes) ? nodes : [])
dynamicProviders = (Array.isArray(nodes) ? (nodes as unknown as ProviderNodeRow[]) : [])
.filter((n: ProviderNodeRow) => {
if (n.apiType !== "chat" && n.apiType !== "responses") return false;
try {

View File

@@ -8,7 +8,7 @@ const reportSchema = z.object({
accountId: z.string().max(120).optional(),
requestId: z.string().max(200).optional(),
errorCode: z.string().max(100).optional(),
details: z.record(z.unknown()).optional(),
details: z.record(z.string(), z.unknown()).optional(),
labels: z.array(z.string().max(50)).optional(),
});

View File

@@ -10,23 +10,24 @@ const limitsSchema = z.object({
});
/**
* GET /api/v1/providers/[id]/limits
* GET /api/v1/providers/[provider]/limits
* Get the current issuance limits for a provider.
*/
export async function GET(request: Request, { params }: { params: { id: string } }) {
export async function GET(request: Request, { params }: { params: Promise<{ provider: string }> }) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
}
const limits = getProviderKeyLimit(params.id);
return NextResponse.json({ provider: params.id, limits: limits ?? null });
const { provider } = await params;
const limits = getProviderKeyLimit(provider);
return NextResponse.json({ provider, limits: limits ?? null });
}
/**
* PUT /api/v1/providers/[id]/limits
* PUT /api/v1/providers/[provider]/limits
* Configure issuance limits for a provider.
*/
export async function PUT(request: Request, { params }: { params: { id: string } }) {
export async function PUT(request: Request, { params }: { params: Promise<{ provider: string }> }) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
}
@@ -43,7 +44,8 @@ export async function PUT(request: Request, { params }: { params: { id: string }
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
}
setProviderKeyLimit(params.id, parsed.data);
const updated = getProviderKeyLimit(params.id);
return NextResponse.json({ provider: params.id, limits: updated });
const { provider } = await params;
setProviderKeyLimit(provider, parsed.data);
const updated = getProviderKeyLimit(provider);
return NextResponse.json({ provider, limits: updated });
}

View File

@@ -7,15 +7,20 @@ import { revokeRegisteredKey } from "@/lib/db/registeredKeys";
*
* Explicit revoke endpoint (supports clients that cannot issue DELETE requests).
*/
export async function POST(request: Request, { params }: { params: { id: string } }) {
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
}
const revoked = revokeRegisteredKey(params.id);
const resolvedParams = await params;
const revoked = revokeRegisteredKey(resolvedParams.id);
if (!revoked) {
return NextResponse.json({ error: "Key not found or already revoked" }, { status: 404 });
}
return NextResponse.json({ success: true, id: params.id, revokedAt: new Date().toISOString() });
return NextResponse.json({
success: true,
id: resolvedParams.id,
revokedAt: new Date().toISOString(),
});
}

View File

@@ -4,12 +4,13 @@ import { getRegisteredKey, revokeRegisteredKey } from "@/lib/db/registeredKeys";
// ─── GET /api/v1/registered-keys/[id] ────────────────────────────────────────
export async function GET(request: Request, { params }: { params: { id: string } }) {
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
}
const key = getRegisteredKey(params.id);
const resolvedParams = await params;
const key = getRegisteredKey(resolvedParams.id);
if (!key) {
return NextResponse.json({ error: "Key not found" }, { status: 404 });
}
@@ -19,15 +20,20 @@ export async function GET(request: Request, { params }: { params: { id: string }
// ─── DELETE /api/v1/registered-keys/[id] ─────────────────────────────────────
export async function DELETE(request: Request, { params }: { params: { id: string } }) {
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: { message: "Authentication required" } }, { status: 401 });
}
const revoked = revokeRegisteredKey(params.id);
const resolvedParams = await params;
const revoked = revokeRegisteredKey(resolvedParams.id);
if (!revoked) {
return NextResponse.json({ error: "Key not found or already revoked" }, { status: 404 });
}
return NextResponse.json({ success: true, id: params.id, revokedAt: new Date().toISOString() });
return NextResponse.json({
success: true,
id: resolvedParams.id,
revokedAt: new Date().toISOString(),
});
}

View File

@@ -1,15 +1,15 @@
import { getDbInstance } from "./core";
interface SecretRow {
value?: unknown;
value?: string;
}
export function getPersistedSecret(key: string): string | null {
try {
const db = getDbInstance();
const row = db
.prepare<SecretRow>("SELECT value FROM key_value WHERE namespace = 'secrets' AND key = ?")
.get(key);
.prepare("SELECT value FROM key_value WHERE namespace = 'secrets' AND key = ?")
.get(key) as SecretRow | undefined;
return typeof row?.value === "string" ? JSON.parse(row.value) : null;
} catch {
return null;
@@ -19,8 +19,9 @@ export function getPersistedSecret(key: string): string | null {
export function persistSecret(key: string, value: string): void {
try {
const db = getDbInstance();
db.prepare("INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('secrets', ?, ?)")
.run(key, JSON.stringify(value));
db.prepare(
"INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('secrets', ?, ?)"
).run(key, JSON.stringify(value));
} catch {
// Non-fatal: secrets still work for the current process if persistence fails.
}

View File

@@ -219,20 +219,7 @@ export const DEFAULT_PRICING = {
reasoning: 18.0,
cache_creation: 2.0,
},
"gemini-3-flash-preview": {
input: 0.5,
output: 3.0,
cached: 0.03,
reasoning: 4.5,
cache_creation: 0.5,
},
"gemini-3.1-flash-lite-preview": {
input: 0.5,
output: 3.0,
cached: 0.03,
reasoning: 4.5,
cache_creation: 0.5,
},
"gemini-2.5-pro": {
input: 2.0,
output: 12.0,