From 4fbe45f30aa5844fe650799e200480d219def43d Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 14 Mar 2026 15:28:53 -0300 Subject: [PATCH] fix: stream timeout, combo test auth, and empty tool name (#355 #350 #346) - fix #355: increase STREAM_IDLE_TIMEOUT_MS from 60s to 300s to prevent premature stream abortion for extended-thinking models (claude-opus-4-6, o3, etc.) that can pause >60s during reasoning phases. Configurable via STREAM_IDLE_TIMEOUT_MS env var. - fix #350: combo health check test now bypasses REQUIRE_API_KEY=true by sending X-Internal-Test header, recognized in chat.ts auth pipeline to skip API key validation for internal admin-side combo tests. Also extended test timeout from 15s to 20s. Uses OpenAI-compatible format universally (not Claude-style). - fix #346: filter out tools with empty function.name before forwarding to upstream providers. Claude Code sends empty-name tool definitions that cause '400 Invalid input[N].name: empty string' on OpenAI-compat providers. Extends existing message/input empty-name filter. --- open-sse/config/constants.ts | 4 +++- open-sse/handlers/chatCore.ts | 10 ++++++++++ src/app/api/combos/test/route.ts | 9 +++++++-- src/sse/handlers/chat.ts | 4 +++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/open-sse/config/constants.ts b/open-sse/config/constants.ts index 29a5d3393d..f310f8ab2c 100644 --- a/open-sse/config/constants.ts +++ b/open-sse/config/constants.ts @@ -4,7 +4,9 @@ import { loadProviderCredentials } from "./credentialLoader.ts"; export const FETCH_TIMEOUT_MS = parseInt(process.env.FETCH_TIMEOUT_MS || "120000", 10); // Idle timeout for SSE streams (ms). Closes stream if no data for this duration. -export const STREAM_IDLE_TIMEOUT_MS = parseInt(process.env.STREAM_IDLE_TIMEOUT_MS || "60000", 10); +// Default: 300s to support extended-thinking models (claude-opus-4-6, o3, etc.) +// that may pause for >60s during deep reasoning phases. Override with STREAM_IDLE_TIMEOUT_MS env var. +export const STREAM_IDLE_TIMEOUT_MS = parseInt(process.env.STREAM_IDLE_TIMEOUT_MS || "300000", 10); // Provider configurations // OAuth credentials read from env vars with hardcoded fallbacks for backward compatibility. diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index afcc2bbc0e..5661765ebd 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -192,6 +192,16 @@ export async function handleChatCore({ return item; }); } + // ── #346: Strip tools with empty function.name ── + // Claude Code sometimes forwards tool definitions with empty names, causing + // OpenAI-compatible upstream providers to reject with: + // "Invalid 'input[N].name': empty string. Expected minimum length 1." + if (Array.isArray(body.tools)) { + body.tools = body.tools.filter((tool: Record) => { + const fn = tool.function as Record | undefined; + return fn?.name && String(fn.name).trim().length > 0; + }); + } translatedBody = translateRequest( sourceFormat, diff --git a/src/app/api/combos/test/route.ts b/src/app/api/combos/test/route.ts index a21aaad98e..3506acb47d 100644 --- a/src/app/api/combos/test/route.ts +++ b/src/app/api/combos/test/route.ts @@ -49,6 +49,7 @@ export async function POST(request) { const startTime = Date.now(); try { // Send a minimal chat request to the internal SSE handler + // Use OpenAI-compatible format — universally accepted by all providers via the translator const testBody = { model: modelStr, messages: [{ role: "user", content: "Hi" }], @@ -58,11 +59,15 @@ export async function POST(request) { const internalUrl = `${getBaseUrl(request)}/v1/chat/completions`; const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 15000); // 15s timeout + const timeout = setTimeout(() => controller.abort(), 20000); // 20s timeout (was 15s, slow providers need more) const res = await fetch(internalUrl, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + // Fix #350: bypass REQUIRE_API_KEY for internal admin combo tests + "X-Internal-Test": "combo-health-check", + }, body: JSON.stringify(testBody), signal: controller.signal, }); diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 742bf107de..f9e5767255 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -133,7 +133,9 @@ export async function handleChat(request: any, clientRawRequest: any = null) { // Optional strict API key mode for /v1 endpoints. // Keep disabled by default to preserve local-mode compatibility. - if (process.env.REQUIRE_API_KEY === "true") { + // Exception: X-Internal-Test header bypasses auth for admin-side combo health checks (#350) + const isInternalTest = request.headers?.get?.("x-internal-test") === "combo-health-check"; + if (process.env.REQUIRE_API_KEY === "true" && !isInternalTest) { if (!apiKey) { log.warn("AUTH", "Missing API key while REQUIRE_API_KEY=true"); return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key");