on main help sign in
Merge pull request #32 from charmbracelet/perf-http-clients
acb1ad1Raphael Amorim
internal/llm/tools/fetch.go+12−6
@@ -69,6 +69,11 @@ func NewFetchTool(permissions permission.Service) BaseTool {
69 return &fetchTool{
69 return &fetchTool{
70 client: &http.Client{
70 client: &http.Client{
71 Timeout: 30 * time.Second,
71 Timeout: 30 * time.Second,
72+ Transport: &http.Transport{
73+ MaxIdleConns: 100,
74+ MaxIdleConnsPerHost: 10,
75+ IdleConnTimeout: 90 * time.Second,
76+ },
72 },
77 },
73 permissions: permissions,
78 permissions: permissions,
74 }
79 }
@@ -136,25 +141,26 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
136 return ToolResponse{}, permission.ErrorPermissionDenied
141 return ToolResponse{}, permission.ErrorPermissionDenied
137 }
142 }
138
143
139− client := t.client
144+ // Handle timeout with context
145+ requestCtx := ctx
140 if params.Timeout > 0 {
146 if params.Timeout > 0 {
141 maxTimeout := 120 // 2 minutes
147 maxTimeout := 120 // 2 minutes
142 if params.Timeout > maxTimeout {
148 if params.Timeout > maxTimeout {
143 params.Timeout = maxTimeout
149 params.Timeout = maxTimeout
144 }
150 }
145− client = &http.Client{
151+ var cancel context.CancelFunc
146− Timeout: time.Duration(params.Timeout) * time.Second,
152+ requestCtx, cancel = context.WithTimeout(ctx, time.Duration(params.Timeout)*time.Second)
147− }
153+ defer cancel()
148 }
154 }
149
155
150− req, err := http.NewRequestWithContext(ctx, "GET", params.URL, nil)
156+ req, err := http.NewRequestWithContext(requestCtx, "GET", params.URL, nil)
151 if err != nil {
157 if err != nil {
152 return ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
158 return ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
153 }
159 }
154
160
155 req.Header.Set("User-Agent", "crush/1.0")
161 req.Header.Set("User-Agent", "crush/1.0")
156
162
157− resp, err := client.Do(req)
163+ resp, err := t.client.Do(req)
158 if err != nil {
164 if err != nil {
159 return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
165 return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
160 }
166 }
internal/llm/tools/grep.go+47
@@ -5,6 +5,7 @@ import (
5 "context"
5 "context"
6 "encoding/json"
6 "encoding/json"
7 "fmt"
7 "fmt"
8+ "io"
8 "os"
9 "os"
9 "os/exec"
10 "os/exec"
10 "path/filepath"
11 "path/filepath"
@@ -377,6 +378,11 @@ func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error
377}
378}
378
379
379func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, int, string, error) {
380func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, int, string, error) {
381+ // Quick binary file detection
382+ if isBinaryFile(filePath) {
383+ return false, 0, "", nil
384+ }
385+
380 file, err := os.Open(filePath)
386 file, err := os.Open(filePath)
381 if err != nil {
387 if err != nil {
382 return false, 0, "", err
388 return false, 0, "", err
@@ -396,6 +402,47 @@ func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, int, st
396 return false, 0, "", scanner.Err()
402 return false, 0, "", scanner.Err()
397}
403}
398
404
405+var binaryExts = map[string]struct{}{
406+ ".exe": {}, ".dll": {}, ".so": {}, ".dylib": {},
407+ ".bin": {}, ".obj": {}, ".o": {}, ".a": {},
408+ ".zip": {}, ".tar": {}, ".gz": {}, ".bz2": {},
409+ ".jpg": {}, ".jpeg": {}, ".png": {}, ".gif": {},
410+ ".pdf": {}, ".doc": {}, ".docx": {}, ".xls": {},
411+ ".mp3": {}, ".mp4": {}, ".avi": {}, ".mov": {},
412+}
413+
414+// isBinaryFile performs a quick check to determine if a file is binary
415+func isBinaryFile(filePath string) bool {
416+ // Check file extension first (fastest)
417+ ext := strings.ToLower(filepath.Ext(filePath))
418+ if _, isBinary := binaryExts[ext]; isBinary {
419+ return true
420+ }
421+
422+ // Quick content check for files without clear extensions
423+ file, err := os.Open(filePath)
424+ if err != nil {
425+ return false // If we can't open it, let the caller handle the error
426+ }
427+ defer file.Close()
428+
429+ // Read first 512 bytes to check for null bytes
430+ buffer := make([]byte, 512)
431+ n, err := file.Read(buffer)
432+ if err != nil && err != io.EOF {
433+ return false
434+ }
435+
436+ // Check for null bytes (common in binary files)
437+ for i := range n {
438+ if buffer[i] == 0 {
439+ return true
440+ }
441+ }
442+
443+ return false
444+}
445+
399func globToRegex(glob string) string {
446func globToRegex(glob string) string {
400 regexPattern := strings.ReplaceAll(glob, ".", "\\.")
447 regexPattern := strings.ReplaceAll(glob, ".", "\\.")
401 regexPattern = strings.ReplaceAll(regexPattern, "*", ".*")
448 regexPattern = strings.ReplaceAll(regexPattern, "*", ".*")
internal/llm/tools/sourcegraph.go+13−6
@@ -129,6 +129,11 @@ func NewSourcegraphTool() BaseTool {
129 return &sourcegraphTool{
129 return &sourcegraphTool{
130 client: &http.Client{
130 client: &http.Client{
131 Timeout: 30 * time.Second,
131 Timeout: 30 * time.Second,
132+ Transport: &http.Transport{
133+ MaxIdleConns: 100,
134+ MaxIdleConnsPerHost: 10,
135+ IdleConnTimeout: 90 * time.Second,
136+ },
132 },
137 },
133 }
138 }
134}
139}
@@ -178,15 +183,17 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
178 if params.ContextWindow <= 0 {
183 if params.ContextWindow <= 0 {
179 params.ContextWindow = 10 // Default context window
184 params.ContextWindow = 10 // Default context window
180 }
185 }
181− client := t.client
186+
187+ // Handle timeout with context
188+ requestCtx := ctx
182 if params.Timeout > 0 {
189 if params.Timeout > 0 {
183 maxTimeout := 120 // 2 minutes
190 maxTimeout := 120 // 2 minutes
184 if params.Timeout > maxTimeout {
191 if params.Timeout > maxTimeout {
185 params.Timeout = maxTimeout
192 params.Timeout = maxTimeout
186 }
193 }
187− client = &http.Client{
194+ var cancel context.CancelFunc
188− Timeout: time.Duration(params.Timeout) * time.Second,
195+ requestCtx, cancel = context.WithTimeout(ctx, time.Duration(params.Timeout)*time.Second)
189− }
196+ defer cancel()
190 }
197 }
191
198
192 type graphqlRequest struct {
199 type graphqlRequest struct {
@@ -208,7 +215,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
208 graphqlQuery := string(graphqlQueryBytes)
215 graphqlQuery := string(graphqlQueryBytes)
209
216
210 req, err := http.NewRequestWithContext(
217 req, err := http.NewRequestWithContext(
211− ctx,
218+ requestCtx,
212 "POST",
219 "POST",
213 "https://sourcegraph.com/.api/graphql",
220 "https://sourcegraph.com/.api/graphql",
214 bytes.NewBuffer([]byte(graphqlQuery)),
221 bytes.NewBuffer([]byte(graphqlQuery)),
@@ -220,7 +227,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
220 req.Header.Set("Content-Type", "application/json")
227 req.Header.Set("Content-Type", "application/json")
221 req.Header.Set("User-Agent", "crush/1.0")
228 req.Header.Set("User-Agent", "crush/1.0")
222
229
223− resp, err := client.Do(req)
230+ resp, err := t.client.Do(req)
224 if err != nil {
231 if err != nil {
225 return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
232 return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
226 }
233 }