diff --git a/open-sse/handlers/responseTranslator.ts b/open-sse/handlers/responseTranslator.ts index 1375d25c06..bce88043db 100644 --- a/open-sse/handlers/responseTranslator.ts +++ b/open-sse/handlers/responseTranslator.ts @@ -151,7 +151,7 @@ export function translateNonStreamingResponse( if (toolCalls.length > 0) { message.tool_calls = toolCalls; } - if (!message.content && !message.tool_calls) { + if (message.content === undefined) { message.content = ""; } @@ -352,7 +352,7 @@ export function translateNonStreamingResponse( if (toolCalls.length > 0) { message.tool_calls = toolCalls; } - if (!message.content && !message.tool_calls) { + if (message.content === undefined) { message.content = ""; } @@ -410,18 +410,31 @@ function convertOpenAINonStreamingToClaude(openaiResponse: JsonRecord): JsonReco const content = []; + let hasTextOrReasoning = false; + if (messageObj.reasoning_content) { + hasTextOrReasoning = true; content.push({ type: "thinking", thinking: toString(messageObj.reasoning_content), }); } - if (messageObj.content) { + // Always include text if it exists (even empty string), or if there are no tool calls and no reasoning + const hasToolCalls = Array.isArray(messageObj.tool_calls) && messageObj.tool_calls.length > 0; + + if (messageObj.content !== undefined && messageObj.content !== null) { + hasTextOrReasoning = true; content.push({ type: "text", text: toString(messageObj.content), }); + } else if (!hasTextOrReasoning) { + // Claude format expects a text block even before tool calls (or if empty) + content.push({ + type: "text", + text: "", + }); } if (Array.isArray(messageObj.tool_calls)) { diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx index a779997e21..4075c078e9 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx @@ -211,7 +211,12 @@ export default function ProviderLimits() { USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) && (conn.authType === "oauth" || conn.authType === "apikey") ); - await Promise.all(usageConnections.map((conn) => fetchQuota(conn.id, conn.provider))); + // Fix Issue #784: Fetch quotas in chunks of 5 to avoid spamming the backend/provider APIs and hanging the UI. + const chunkSize = 5; + for (let i = 0; i < usageConnections.length; i += chunkSize) { + const chunk = usageConnections.slice(i, i + chunkSize); + await Promise.all(chunk.map((conn) => fetchQuota(conn.id, conn.provider))); + } setLastUpdated(new Date()); } catch (error) { console.error("Error refreshing all:", error); diff --git a/src/shared/components/OAuthModal.tsx b/src/shared/components/OAuthModal.tsx index 6a4c73c02f..340c2d5fff 100644 --- a/src/shared/components/OAuthModal.tsx +++ b/src/shared/components/OAuthModal.tsx @@ -211,6 +211,8 @@ export default function OAuthModal({ return; } + let forceManual = false; + // Codex: on localhost use callback server on port 1455, // on remote use standard auth code flow (callback server is unreachable) if (provider === "codex") { @@ -252,11 +254,13 @@ export default function OAuthModal({ setPolling(false); throw new Error("Authorization timeout"); } catch (codexErr) { + console.warn( + "Codex callback server failed, falling back to standard manual flow", + codexErr + ); setPolling(false); - setStep("input"); - setError(codexErr.message + " — You can paste the callback URL manually below."); + forceManual = true; } - return; } // Remote: fall through to standard auth code flow below } @@ -306,8 +310,8 @@ export default function OAuthModal({ setAuthData({ ...data, redirectUri }); - // For non-true-localhost (LAN IPs, remote): use manual input mode (user pastes callback URL) - if (!isTrueLocalhost) { + // For non-true-localhost (LAN IPs, remote) or manual fallback: use manual input mode (user pastes callback URL) + if (!isTrueLocalhost || forceManual) { setStep("input"); window.open(data.authUrl, "oauth_auth"); } else { diff --git a/src/shared/services/cliRuntime.ts b/src/shared/services/cliRuntime.ts index 2c06854914..ff658bce61 100644 --- a/src/shared/services/cliRuntime.ts +++ b/src/shared/services/cliRuntime.ts @@ -328,10 +328,7 @@ const getExpectedParentPaths = (): string[] => { const npmPrefix = getNpmGlobalPrefix(); // Add common user bin directories - const userBinPaths = [ - path.join(home, "bin"), - path.join(home, ".local", "bin"), - ]; + const userBinPaths = [path.join(home, "bin"), path.join(home, ".local", "bin")]; return [ home, @@ -531,11 +528,15 @@ const locateCommand = async (command: string, env: Record normalizeMsys2Path(line.trim())) - .find(Boolean) || null; + const lines = located.stdout + .split(/\r?\n/) + .map((line) => normalizeMsys2Path(line.trim())) + .filter(Boolean); + + // Issue #809: Prioritize executable wrappers (.cmd, .exe, .bat) over extensionless bash scripts + // that NPM often drops alongside the wrappers in global installs. + const first = lines.find((line) => /\.(cmd|exe|bat)$/i.test(line)) || lines[0] || null; + return { installed: !!first, commandPath: first, reason: first ? null : "not_found" }; }