From 73bda23c6000c49f9a62a75d2f53712641d310a2 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 10 May 2026 09:10:43 -0300 Subject: [PATCH] chore(release): finalize v3.8.0 stabilization and fix typescript regressions - Fix stream readiness loop and upstream error code propagation in chatCore.ts - Resolve Headers iterator TypeScript errors - Fix type mismatches and missing props in BuilderIntelligentStep, Card, and providers page - Fix providerLimits typecasts and resolve implicit any errors - Ensure green build and strict type compliance for production --- CHANGELOG.md | 5 ++ Dockerfile | 6 +- open-sse/executors/pollinations.ts | 4 +- open-sse/handlers/chatCore.ts | 74 ++++++++++++++++--- .../helpers/geminiToolsSanitizer.ts | 16 ++-- open-sse/utils/error.ts | 25 ++++++- .../combos/BuilderIntelligentStep.tsx | 19 ++++- .../dashboard/providers/[id]/page.tsx | 5 +- src/app/api/providers/[id]/models/route.ts | 14 ++-- src/app/api/providers/[id]/route.ts | 9 ++- src/app/api/providers/route.ts | 6 +- src/lib/db/settings.ts | 51 ++++++++++--- src/lib/embeddings/service.ts | 2 +- src/lib/usage/providerLimits.ts | 6 +- src/mitm/cert/install.ts | 22 +++++- src/shared/components/Card.tsx | 24 +++++- 16 files changed, 227 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4720b7bd53..4762c9c4b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ ### 🐛 Bug Fixes +- **fix(pricing):** make `getPricingForModel` fully case-insensitive to ensure custom prices correctly reflect in new incoming requests cost calculations +- **fix(gemini):** prevent `functionDeclarations` from being dropped by the sanitizer when `googleSearch` tool is present (#2077) +- **fix(pollinations):** add `jsonMode: true` flag in the request transformation to enforce correct JSON structure from Pollinations API (#2109) +- **fix(docker):** update Dockerfile to copy `/docs` directory during build ensuring API catalog availability at runtime (#2083) - **fix(providers):** strip OpenAI-specific fields in Kiro translator to prevent 400 errors (#2037) - **fix(ui):** resolve text contrast issues for zero-config warning banner in light mode (#2050) - **fix(core):** inject global system prompt correctly into downstream chat completions pipeline (#2080) @@ -25,6 +29,7 @@ ### 🔒 Security +- **fix(security):** remediate CodeQL vulnerabilities (ReDoS, cryptographic bias, stack trace exposure, and weak password hashing) (#216, #215, #211, #208, #206, #210) - **fix(security):** remediate regex validation backtracking path in core compression cleanup (#1990) - **fix(core):** harden input handling and stabilization for prompt compression edge cases diff --git a/Dockerfile b/Dockerfile index ee090131d8..7500434464 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,9 +56,9 @@ COPY --from=builder /app/src/lib/db/migrations ./migrations ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations # MITM server.cjs is spawned at runtime via child_process — not traced by nft COPY --from=builder /app/src/mitm/server.cjs ./src/mitm/server.cjs -# OpenAPI spec is read from disk by /api/openapi/spec at runtime for the -# Endpoints dashboard. Next.js standalone tracing does not include it. -COPY --from=builder /app/docs/openapi.yaml ./docs/openapi.yaml +# Documentation files and OpenAPI spec are read from disk at runtime. +# Next.js standalone tracing does not include them. +COPY --from=builder /app/docs ./docs COPY --from=builder /app/scripts/run-standalone.mjs ./run-standalone.mjs COPY --from=builder /app/scripts/runtime-env.mjs ./runtime-env.mjs diff --git a/open-sse/executors/pollinations.ts b/open-sse/executors/pollinations.ts index 653026fdf3..cd6219d8f1 100644 --- a/open-sse/executors/pollinations.ts +++ b/open-sse/executors/pollinations.ts @@ -42,7 +42,9 @@ export class PollinationsExecutor extends BaseExecutor { } transformRequest(model: string, body: any, _stream: boolean, _credentials: any): any { - // Pollinations uses provider aliases directly: "openai", "claude", "gemini", etc. + if (typeof body === "object" && body !== null) { + body.jsonMode = true; + } return body; } } diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 2cf6762d37..74b9d7622e 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -2994,7 +2994,11 @@ export async function handleChatCore({ _dedupSnapshot: { status, statusText, - headers: Array.from(headers.entries()), + headers: (() => { + const arr: [string, string][] = []; + headers.forEach((v, k) => arr.push([k, v])); + return arr; + })(), payload, }, }; @@ -3249,6 +3253,8 @@ export async function handleChatCore({ let statusCode = providerResponse.status; let message = ""; let retryAfterMs: number | null = null; + let upstreamErrorCode: string | undefined; + let upstreamErrorType: string | undefined; if (upstreamErrorParsed) { statusCode = parsedStatusCode; @@ -3260,6 +3266,8 @@ export async function handleChatCore({ message = details.message; retryAfterMs = details.retryAfterMs; upstreamErrorBody = details.responseBody; + upstreamErrorCode = details.errorCode; + upstreamErrorType = details.errorType; } // T06/T10/T36: classify provider errors and persist terminal account states. @@ -3428,7 +3436,13 @@ export async function handleChatCore({ cacheSource: "upstream", }); persistFailureUsage(statusCode, "model_unavailable"); - return createErrorResult(statusCode, errMsg, retryAfterMs); + return createErrorResult( + statusCode, + errMsg, + retryAfterMs, + upstreamErrorCode, + upstreamErrorType + ); } } catch { persistAttemptLogs({ @@ -3440,7 +3454,13 @@ export async function handleChatCore({ cacheSource: "upstream", }); persistFailureUsage(statusCode, "model_unavailable"); - return createErrorResult(statusCode, errMsg, retryAfterMs); + return createErrorResult( + statusCode, + errMsg, + retryAfterMs, + upstreamErrorCode, + upstreamErrorType + ); } } else { persistAttemptLogs({ @@ -3452,7 +3472,13 @@ export async function handleChatCore({ cacheSource: "upstream", }); persistFailureUsage(statusCode, "model_unavailable"); - return createErrorResult(statusCode, errMsg, retryAfterMs); + return createErrorResult( + statusCode, + errMsg, + retryAfterMs, + upstreamErrorCode, + upstreamErrorType + ); } } else if (isContextOverflowError(statusCode, message)) { const familyCandidates = getModelFamily(currentModel).filter( @@ -3488,7 +3514,13 @@ export async function handleChatCore({ cacheSource: "upstream", }); persistFailureUsage(statusCode, "context_overflow"); - return createErrorResult(statusCode, errMsg, retryAfterMs); + return createErrorResult( + statusCode, + errMsg, + retryAfterMs, + upstreamErrorCode, + upstreamErrorType + ); } } catch { persistAttemptLogs({ @@ -3500,7 +3532,13 @@ export async function handleChatCore({ cacheSource: "upstream", }); persistFailureUsage(statusCode, "context_overflow"); - return createErrorResult(statusCode, errMsg, retryAfterMs); + return createErrorResult( + statusCode, + errMsg, + retryAfterMs, + upstreamErrorCode, + upstreamErrorType + ); } } else { persistAttemptLogs({ @@ -3512,7 +3550,13 @@ export async function handleChatCore({ cacheSource: "upstream", }); persistFailureUsage(statusCode, "context_overflow"); - return createErrorResult(statusCode, errMsg, retryAfterMs); + return createErrorResult( + statusCode, + errMsg, + retryAfterMs, + upstreamErrorCode, + upstreamErrorType + ); } } else { persistAttemptLogs({ @@ -3595,7 +3639,13 @@ export async function handleChatCore({ } if (!emergencyFallbackServed) { - return createErrorResult(statusCode, errMsg, retryAfterMs); + return createErrorResult( + statusCode, + errMsg, + retryAfterMs, + upstreamErrorCode, + upstreamErrorType + ); } } // ── End T5 ─────────────────────────────────────────────────────────────── @@ -4067,9 +4117,11 @@ export async function handleChatCore({ const responseHeaders: Record = { ...Object.fromEntries( - Array.from(providerResponse.headers.entries()).filter( - ([k]) => k.toLowerCase() !== "content-type" - ) + (() => { + const arr: [string, string][] = []; + providerResponse.headers.forEach((v, k) => arr.push([k, v])); + return arr; + })().filter(([k]) => k.toLowerCase() !== "content-type") ), "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", diff --git a/open-sse/translator/helpers/geminiToolsSanitizer.ts b/open-sse/translator/helpers/geminiToolsSanitizer.ts index d76f23cf7c..3d36dc3468 100644 --- a/open-sse/translator/helpers/geminiToolsSanitizer.ts +++ b/open-sse/translator/helpers/geminiToolsSanitizer.ts @@ -209,19 +209,15 @@ export function buildGeminiTools( } } - if (googleSearchTool && functionDeclarations.length > 0) { - console.warn( - `[GeminiTools] Removing ${functionDeclarations.length} functionDeclarations because googleSearch cannot be mixed with Gemini function tools` - ); + const result: GeminiTool[] = []; + + if (functionDeclarations.length > 0) { + result.push({ functionDeclarations }); } if (googleSearchTool) { - return [googleSearchTool]; + result.push(googleSearchTool); } - if (functionDeclarations.length > 0) { - return [{ functionDeclarations }]; - } - - return undefined; + return result.length > 0 ? result : undefined; } diff --git a/open-sse/utils/error.ts b/open-sse/utils/error.ts index 2b28cc7196..05b5c32755 100644 --- a/open-sse/utils/error.ts +++ b/open-sse/utils/error.ts @@ -116,6 +116,8 @@ export async function parseUpstreamError(response, provider = null) { let message = ""; let retryAfterMs = null; let responseBody = null; + let errorCode = undefined; + let errorType = undefined; try { const text = await response.text(); @@ -125,6 +127,8 @@ export async function parseUpstreamError(response, provider = null) { try { const json = JSON.parse(text); message = json.error?.message || json.message || json.error || text; + errorCode = json.error?.code || json.code; + errorType = json.error?.type || json.type; } catch { message = text; } @@ -179,6 +183,8 @@ export async function parseUpstreamError(response, provider = null) { return { statusCode: response.status, message: messageStr, + errorCode, + errorType, retryAfterMs, responseBody, responseHeaders, @@ -195,19 +201,34 @@ export async function parseUpstreamError(response, provider = null) { export function createErrorResult( statusCode: number, message: string, - retryAfterMs: number | null = null + retryAfterMs: number | null = null, + errorCode?: string, + errorType?: string ) { + const body = buildErrorBody(statusCode, message); + if (errorCode) { + (body.error as any).code = errorCode; + } + if (errorType) { + (body.error as any).type = errorType; + } + const result: { success: false; status: number; error: string; + errorType?: string; response: Response; retryAfterMs?: number; } = { success: false, status: statusCode, error: message, - response: errorResponse(statusCode, message), + errorType, + response: new Response(JSON.stringify(body), { + status: statusCode, + headers: { "Content-Type": "application/json" }, + }), }; // Add retryAfterMs if available (for Antigravity quota errors) diff --git a/src/app/(dashboard)/dashboard/combos/BuilderIntelligentStep.tsx b/src/app/(dashboard)/dashboard/combos/BuilderIntelligentStep.tsx index 3dabb89695..087aba2fbd 100644 --- a/src/app/(dashboard)/dashboard/combos/BuilderIntelligentStep.tsx +++ b/src/app/(dashboard)/dashboard/combos/BuilderIntelligentStep.tsx @@ -15,7 +15,7 @@ function getI18nOrFallback(t: any, key: string, fallback: string) { return fallback; } -function toProviderOptions(activeProviders: any[] = []) { +function toProviderOptions(activeProviders: any[] = [], candidatePool: string[] = []) { const uniqueProviders = new Map(); activeProviders.forEach((provider) => { @@ -41,6 +41,16 @@ function toProviderOptions(activeProviders: any[] = []) { }); }); + candidatePool.forEach((poolId) => { + if (!uniqueProviders.has(poolId)) { + uniqueProviders.set(poolId, { + id: poolId, + label: `${poolId} (Offline/Deleted)`, + connectionCount: 0, + }); + } + }); + return [...uniqueProviders.values()].sort((a, b) => a.label.localeCompare(b.label)); } @@ -56,7 +66,10 @@ export default function BuilderIntelligentStep({ activeProviders: any[]; }) { const normalizedConfig = normalizeIntelligentRoutingConfig(config); - const providerOptions = useMemo(() => toProviderOptions(activeProviders), [activeProviders]); + const providerOptions = useMemo( + () => toProviderOptions(activeProviders, normalizedConfig.candidatePool), + [activeProviders, normalizedConfig.candidatePool] + ); const updateConfig = (patch: Record) => { onChange({ @@ -64,7 +77,7 @@ export default function BuilderIntelligentStep({ ...patch, weights: { ...normalizedConfig.weights, - ...(patch.weights || {}), + ...((patch.weights as Record) || {}), }, }); }; diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index e1e4300c90..5a6a96491c 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -509,6 +509,7 @@ interface ConnectionRowConnection { expiresAt?: string; tokenExpiresAt?: string; maxConcurrent?: number | null; + authType?: string; } interface ConnectionRowProps { @@ -3024,7 +3025,7 @@ export default function ProviderDetailPage() { {selectedIds.size > 0 && (