From 089d1299f545f668dac7d8ad042da6ae6ffab6fd Mon Sep 17 00:00:00 2001 From: Hussain Date: Thu, 19 Feb 2026 02:44:23 +0800 Subject: [PATCH] providers: increase local openai-compatible timeout for ollama --- pkg/providers/openai_compat/provider.go | 38 +++++++++++++++++++- pkg/providers/openai_compat/provider_test.go | 20 +++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index 9b404dd77..5fc765812 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -29,9 +29,22 @@ type Provider struct { httpClient *http.Client } +const ( + defaultRequestTimeout = 120 * time.Second + localRequestTimeout = 10 * time.Minute +) + func NewProvider(apiKey, apiBase, proxy string) *Provider { + return NewProviderWithTimeout(apiKey, apiBase, proxy, 0) +} + +// NewProviderWithTimeout creates a provider with optional custom timeout seconds. +// timeoutSeconds <= 0 uses defaults: +// - local endpoints (localhost/127.0.0.1/::1): 10 minutes +// - other endpoints: 120 seconds +func NewProviderWithTimeout(apiKey, apiBase, proxy string, timeoutSeconds int) *Provider { client := &http.Client{ - Timeout: 120 * time.Second, + Timeout: resolveRequestTimeout(apiBase, timeoutSeconds), } if proxy != "" { @@ -52,6 +65,29 @@ func NewProvider(apiKey, apiBase, proxy string) *Provider { } } +func resolveRequestTimeout(apiBase string, timeoutSeconds int) time.Duration { + if timeoutSeconds > 0 { + return time.Duration(timeoutSeconds) * time.Second + } + if isLocalAPIBase(apiBase) { + return localRequestTimeout + } + return defaultRequestTimeout +} + +func isLocalAPIBase(apiBase string) bool { + parsed, err := url.Parse(apiBase) + if err != nil { + lower := strings.ToLower(apiBase) + return strings.Contains(lower, "localhost") || + strings.Contains(lower, "127.0.0.1") || + strings.Contains(lower, "::1") + } + + host := strings.ToLower(parsed.Hostname()) + return host == "localhost" || host == "127.0.0.1" || host == "::1" +} + func (p *Provider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) { if p.apiBase == "" { return nil, fmt.Errorf("API base not configured") diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go index 94779b39c..8049934db 100644 --- a/pkg/providers/openai_compat/provider_test.go +++ b/pkg/providers/openai_compat/provider_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "net/url" "testing" + "time" ) func TestProviderChat_UsesMaxCompletionTokensForGLM(t *testing.T) { @@ -275,3 +276,22 @@ func TestNormalizeModel_UsesAPIBase(t *testing.T) { t.Fatalf("normalizeModel(openrouter) = %q, want %q", got, "openrouter/auto") } } + +func TestProvider_TimeoutDefaults(t *testing.T) { + pRemote := NewProvider("key", "https://api.openai.com/v1", "") + if pRemote.httpClient.Timeout != defaultRequestTimeout { + t.Fatalf("remote timeout = %v, want %v", pRemote.httpClient.Timeout, defaultRequestTimeout) + } + + pLocal := NewProvider("key", "http://127.0.0.1:11434/v1", "") + if pLocal.httpClient.Timeout != localRequestTimeout { + t.Fatalf("local timeout = %v, want %v", pLocal.httpClient.Timeout, localRequestTimeout) + } +} + +func TestProvider_CustomTimeoutOverride(t *testing.T) { + p := NewProviderWithTimeout("key", "http://127.0.0.1:11434/v1", "", 45) + if p.httpClient.Timeout != 45*time.Second { + t.Fatalf("timeout = %v, want %v", p.httpClient.Timeout, 45*time.Second) + } +}