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");