diff --git a/next.config.mjs b/next.config.mjs index f9d695a1f1..b7f4645d40 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -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", diff --git a/open-sse/mcp-server/schemas/a2a.ts b/open-sse/mcp-server/schemas/a2a.ts index 9be4d50eb4..a96fcb1553 100644 --- a/open-sse/mcp-server/schemas/a2a.ts +++ b/open-sse/mcp-server/schemas/a2a.ts @@ -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; 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({ diff --git a/src/app/(dashboard)/dashboard/search-tools/SearchToolsClient.tsx b/src/app/(dashboard)/dashboard/search-tools/SearchToolsClient.tsx index 52f3ff9be7..3caa463b0c 100644 --- a/src/app/(dashboard)/dashboard/search-tools/SearchToolsClient.tsx +++ b/src/app/(dashboard)/dashboard/search-tools/SearchToolsClient.tsx @@ -39,6 +39,7 @@ interface SearchResponse { id: string; provider: string; query: string; + answer?: string; results: SearchResult[]; cached: boolean; usage: { diff --git a/src/app/(dashboard)/dashboard/search-tools/components/ResultsPanel.tsx b/src/app/(dashboard)/dashboard/search-tools/components/ResultsPanel.tsx index b2749c719e..a55b31baf9 100644 --- a/src/app/(dashboard)/dashboard/search-tools/components/ResultsPanel.tsx +++ b/src/app/(dashboard)/dashboard/search-tools/components/ResultsPanel.tsx @@ -20,7 +20,7 @@ interface SearchResponse { provider: string; results: SearchResult[]; query: string; - answer: string | null; + answer?: string | null; cached: boolean; usage: { queries_used: number; diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx index 6f535e13b1..07bca9cfad 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx @@ -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) => { diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx index a7fcd11411..b5861de700 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx @@ -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; diff --git a/src/app/api/settings/codex-service-tier/route.ts b/src/app/api/settings/codex-service-tier/route.ts index 3c980785af..8ec575ed06 100644 --- a/src/app/api/settings/codex-service-tier/route.ts +++ b/src/app/api/settings/codex-service-tier/route.ts @@ -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); diff --git a/src/app/api/settings/proxy/route.ts b/src/app/api/settings/proxy/route.ts index f8549541b5..596bdaac66 100644 --- a/src/app/api/settings/proxy/route.ts +++ b/src/app/api/settings/proxy/route.ts @@ -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 }); } } diff --git a/src/app/api/settings/task-routing/route.ts b/src/app/api/settings/task-routing/route.ts index d86fbc2c7b..adefe3dab5 100644 --- a/src/app/api/settings/task-routing/route.ts +++ b/src/app/api/settings/task-routing/route.ts @@ -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); diff --git a/src/app/api/usage/analytics/route.ts b/src/app/api/usage/analytics/route.ts index 3c73df6243..1050fcfa3d 100644 --- a/src/app/api/usage/analytics/route.ts +++ b/src/app/api/usage/analytics/route.ts @@ -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 { diff --git a/src/app/api/v1/accounts/[id]/limits/route.ts b/src/app/api/v1/accounts/[id]/limits/route.ts index c7bbcdd6e7..8fcd8036ca 100644 --- a/src/app/api/v1/accounts/[id]/limits/route.ts +++ b/src/app/api/v1/accounts/[id]/limits/route.ts @@ -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 }); } diff --git a/src/app/api/v1/audio/speech/route.ts b/src/app/api/v1/audio/speech/route.ts index f53e218067..df1b1cf201 100644 --- a/src/app/api/v1/audio/speech/route.ts +++ b/src/app/api/v1/audio/speech/route.ts @@ -65,7 +65,7 @@ export async function POST(request) { let dynamicProviders: ReturnType[] = []; 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 { diff --git a/src/app/api/v1/audio/transcriptions/route.ts b/src/app/api/v1/audio/transcriptions/route.ts index c0392b646f..1ca1139bbe 100644 --- a/src/app/api/v1/audio/transcriptions/route.ts +++ b/src/app/api/v1/audio/transcriptions/route.ts @@ -63,7 +63,7 @@ export async function POST(request) { let dynamicProviders: ReturnType[] = []; 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 { diff --git a/src/app/api/v1/issues/report/route.ts b/src/app/api/v1/issues/report/route.ts index 11cc130c7b..cd90de458c 100644 --- a/src/app/api/v1/issues/report/route.ts +++ b/src/app/api/v1/issues/report/route.ts @@ -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(), }); diff --git a/src/app/api/v1/providers/[id]/limits/route.ts b/src/app/api/v1/providers/[provider]/limits/route.ts similarity index 72% rename from src/app/api/v1/providers/[id]/limits/route.ts rename to src/app/api/v1/providers/[provider]/limits/route.ts index a249233593..cd2981e604 100644 --- a/src/app/api/v1/providers/[id]/limits/route.ts +++ b/src/app/api/v1/providers/[provider]/limits/route.ts @@ -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 }); } diff --git a/src/app/api/v1/registered-keys/[id]/revoke/route.ts b/src/app/api/v1/registered-keys/[id]/revoke/route.ts index d912c7d1e2..5520c1a26c 100644 --- a/src/app/api/v1/registered-keys/[id]/revoke/route.ts +++ b/src/app/api/v1/registered-keys/[id]/revoke/route.ts @@ -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(), + }); } diff --git a/src/app/api/v1/registered-keys/[id]/route.ts b/src/app/api/v1/registered-keys/[id]/route.ts index 81f6ac22a3..ebdea0f494 100644 --- a/src/app/api/v1/registered-keys/[id]/route.ts +++ b/src/app/api/v1/registered-keys/[id]/route.ts @@ -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(), + }); } diff --git a/src/lib/db/secrets.ts b/src/lib/db/secrets.ts index 443f03de37..bf90f57b8e 100644 --- a/src/lib/db/secrets.ts +++ b/src/lib/db/secrets.ts @@ -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("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. } diff --git a/src/shared/constants/pricing.ts b/src/shared/constants/pricing.ts index aa7af82945..62fdd82c82 100644 --- a/src/shared/constants/pricing.ts +++ b/src/shared/constants/pricing.ts @@ -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,