summaryrefslogtreecommitdiff
path: root/internal/llm
diff options
context:
space:
mode:
authorFlorian <2320560+florianbuetow@users.noreply.github.com>2026-01-31 23:48:38 +0100
committerFlorian <2320560+florianbuetow@users.noreply.github.com>2026-01-31 23:48:38 +0100
commitde37689f2b52665ca87224d6c22b1ebe2280c811 (patch)
treee69b810f813955e24b79409bc27ff6b86adf11e3 /internal/llm
parent28e2d1a7729e4d434e47006a1932eeb75821aadc (diff)
feat: add configurable request timeout for LLM calls
Local LLMs (LM Studio, Ollama, etc.) often need more than the default 30-second timeout. Added request_timeout config option (in seconds) to [general] section and HEXAI_REQUEST_TIMEOUT env var. Original constructor signatures preserved via *WithTimeout variants, so no test changes required.
Diffstat (limited to 'internal/llm')
-rw-r--r--internal/llm/anthropic.go9
-rw-r--r--internal/llm/copilot.go9
-rw-r--r--internal/llm/ollama.go9
-rw-r--r--internal/llm/openai.go9
-rw-r--r--internal/llm/openrouter.go9
-rw-r--r--internal/llm/provider.go13
6 files changed, 47 insertions, 11 deletions
diff --git a/internal/llm/anthropic.go b/internal/llm/anthropic.go
index c0cdc9a..a6c1454 100644
--- a/internal/llm/anthropic.go
+++ b/internal/llm/anthropic.go
@@ -90,14 +90,21 @@ var (
// newAnthropic constructs an Anthropic client using explicit configuration values.
// The apiKey may be empty; calls will fail until a valid key is supplied.
func newAnthropic(baseURL, model, apiKey string, defaultTemp *float64) Client {
+ return newAnthropicWithTimeout(baseURL, model, apiKey, defaultTemp, 0)
+}
+
+func newAnthropicWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) Client {
if strings.TrimSpace(baseURL) == "" {
baseURL = "https://api.anthropic.com/v1"
}
if strings.TrimSpace(model) == "" {
model = "claude-3-5-sonnet-20241022"
}
+ if timeoutSec <= 0 {
+ timeoutSec = 30
+ }
return anthropicClient{
- httpClient: &http.Client{Timeout: 30 * time.Second},
+ httpClient: &http.Client{Timeout: time.Duration(timeoutSec) * time.Second},
apiKey: apiKey,
baseURL: baseURL,
defaultModel: model,
diff --git a/internal/llm/copilot.go b/internal/llm/copilot.go
index b439ed3..43419ea 100644
--- a/internal/llm/copilot.go
+++ b/internal/llm/copilot.go
@@ -64,6 +64,10 @@ type copilotChatResponse struct {
// Constructor (kept among the first functions by convention)
func newCopilot(baseURL, model, apiKey string, defaultTemp *float64) Client {
+ return newCopilotWithTimeout(baseURL, model, apiKey, defaultTemp, 0)
+}
+
+func newCopilotWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) Client {
if strings.TrimSpace(baseURL) == "" {
baseURL = "https://api.githubcopilot.com"
}
@@ -72,8 +76,11 @@ func newCopilot(baseURL, model, apiKey string, defaultTemp *float64) Client {
// Default to a broadly available, cost-effective option.
model = "gpt-4o-mini"
}
+ if timeoutSec <= 0 {
+ timeoutSec = 30
+ }
return copilotClient{
- httpClient: &http.Client{Timeout: 30 * time.Second},
+ httpClient: &http.Client{Timeout: time.Duration(timeoutSec) * time.Second},
apiKey: apiKey,
baseURL: strings.TrimRight(baseURL, "/"),
defaultModel: model,
diff --git a/internal/llm/ollama.go b/internal/llm/ollama.go
index f355166..a22dd7b 100644
--- a/internal/llm/ollama.go
+++ b/internal/llm/ollama.go
@@ -42,14 +42,21 @@ type ollamaChatResponse struct {
// Constructor (kept among the first functions by convention)
func newOllama(baseURL, model string, defaultTemp *float64) Client {
+ return newOllamaWithTimeout(baseURL, model, defaultTemp, 0)
+}
+
+func newOllamaWithTimeout(baseURL, model string, defaultTemp *float64, timeoutSec int) Client {
if strings.TrimSpace(baseURL) == "" {
baseURL = "http://localhost:11434"
}
if strings.TrimSpace(model) == "" {
model = "qwen3-coder:30b-a3b-q4_K_M"
}
+ if timeoutSec <= 0 {
+ timeoutSec = 30
+ }
return ollamaClient{
- httpClient: &http.Client{Timeout: 30 * time.Second},
+ httpClient: &http.Client{Timeout: time.Duration(timeoutSec) * time.Second},
baseURL: strings.TrimRight(baseURL, "/"),
defaultModel: model,
chatLogger: logging.NewChatLogger("ollama"),
diff --git a/internal/llm/openai.go b/internal/llm/openai.go
index b97111d..6bc3a7c 100644
--- a/internal/llm/openai.go
+++ b/internal/llm/openai.go
@@ -77,14 +77,21 @@ type oaStreamChunk struct {
// newOpenAI constructs an OpenAI client using explicit configuration values.
// The apiKey may be empty; calls will fail until a valid key is supplied.
func newOpenAI(baseURL, model, apiKey string, defaultTemp *float64) Client {
+ return newOpenAIWithTimeout(baseURL, model, apiKey, defaultTemp, 0)
+}
+
+func newOpenAIWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) Client {
if strings.TrimSpace(baseURL) == "" {
baseURL = "https://api.openai.com/v1"
}
if strings.TrimSpace(model) == "" {
model = "gpt-4.1"
}
+ if timeoutSec <= 0 {
+ timeoutSec = 30
+ }
return openAIClient{
- httpClient: &http.Client{Timeout: 30 * time.Second},
+ httpClient: &http.Client{Timeout: time.Duration(timeoutSec) * time.Second},
apiKey: apiKey,
baseURL: baseURL,
defaultModel: model,
diff --git a/internal/llm/openrouter.go b/internal/llm/openrouter.go
index 4aae398..21e3102 100644
--- a/internal/llm/openrouter.go
+++ b/internal/llm/openrouter.go
@@ -23,14 +23,21 @@ type openRouterClient struct {
}
func newOpenRouter(baseURL, model, apiKey string, defaultTemp *float64) Client {
+ return newOpenRouterWithTimeout(baseURL, model, apiKey, defaultTemp, 0)
+}
+
+func newOpenRouterWithTimeout(baseURL, model, apiKey string, defaultTemp *float64, timeoutSec int) Client {
if strings.TrimSpace(baseURL) == "" {
baseURL = "https://openrouter.ai/api/v1"
}
if strings.TrimSpace(model) == "" {
model = "openrouter/auto"
}
+ if timeoutSec <= 0 {
+ timeoutSec = 30
+ }
return openRouterClient{
- httpClient: &http.Client{Timeout: 30 * time.Second},
+ httpClient: &http.Client{Timeout: time.Duration(timeoutSec) * time.Second},
apiKey: apiKey,
baseURL: baseURL,
defaultModel: model,
diff --git a/internal/llm/provider.go b/internal/llm/provider.go
index ae840b0..297f1f3 100644
--- a/internal/llm/provider.go
+++ b/internal/llm/provider.go
@@ -64,7 +64,8 @@ func WithStop(stop ...string) RequestOption {
// Config defines provider configuration read from the Hexai config file.
type Config struct {
- Provider string
+ Provider string
+ RequestTimeout int // seconds; 0 means use default (30s)
// OpenAI options
OpenAIBaseURL string
OpenAIModel string
@@ -119,7 +120,7 @@ func NewFromConfig(cfg Config, openAIAPIKey, openRouterAPIKey, copilotAPIKey, an
v := 0.2
cfg.OpenAITemperature = &v
}
- return newOpenAI(cfg.OpenAIBaseURL, cfg.OpenAIModel, openAIAPIKey, cfg.OpenAITemperature), nil
+ return newOpenAIWithTimeout(cfg.OpenAIBaseURL, cfg.OpenAIModel, openAIAPIKey, cfg.OpenAITemperature, cfg.RequestTimeout), nil
case "openrouter":
if strings.TrimSpace(openRouterAPIKey) == "" {
return nil, errors.New("missing OPENROUTER_API_KEY for provider openrouter")
@@ -128,13 +129,13 @@ func NewFromConfig(cfg Config, openAIAPIKey, openRouterAPIKey, copilotAPIKey, an
t := 0.2
cfg.OpenRouterTemperature = &t
}
- return newOpenRouter(cfg.OpenRouterBaseURL, cfg.OpenRouterModel, openRouterAPIKey, cfg.OpenRouterTemperature), nil
+ return newOpenRouterWithTimeout(cfg.OpenRouterBaseURL, cfg.OpenRouterModel, openRouterAPIKey, cfg.OpenRouterTemperature, cfg.RequestTimeout), nil
case "ollama":
if cfg.OllamaTemperature == nil {
t := 0.2
cfg.OllamaTemperature = &t
}
- return newOllama(cfg.OllamaBaseURL, cfg.OllamaModel, cfg.OllamaTemperature), nil
+ return newOllamaWithTimeout(cfg.OllamaBaseURL, cfg.OllamaModel, cfg.OllamaTemperature, cfg.RequestTimeout), nil
case "copilot":
if strings.TrimSpace(copilotAPIKey) == "" {
return nil, errors.New("missing COPILOT_API_KEY for provider copilot")
@@ -143,7 +144,7 @@ func NewFromConfig(cfg Config, openAIAPIKey, openRouterAPIKey, copilotAPIKey, an
t := 0.2
cfg.CopilotTemperature = &t
}
- return newCopilot(cfg.CopilotBaseURL, cfg.CopilotModel, copilotAPIKey, cfg.CopilotTemperature), nil
+ return newCopilotWithTimeout(cfg.CopilotBaseURL, cfg.CopilotModel, copilotAPIKey, cfg.CopilotTemperature, cfg.RequestTimeout), nil
case "anthropic":
if strings.TrimSpace(anthropicAPIKey) == "" {
return nil, errors.New("missing ANTHROPIC_API_KEY for provider anthropic")
@@ -152,7 +153,7 @@ func NewFromConfig(cfg Config, openAIAPIKey, openRouterAPIKey, copilotAPIKey, an
t := 0.2
cfg.AnthropicTemperature = &t
}
- return newAnthropic(cfg.AnthropicBaseURL, cfg.AnthropicModel, anthropicAPIKey, cfg.AnthropicTemperature), nil
+ return newAnthropicWithTimeout(cfg.AnthropicBaseURL, cfg.AnthropicModel, anthropicAPIKey, cfg.AnthropicTemperature, cfg.RequestTimeout), nil
default:
return nil, errors.New("unknown LLM provider: " + p)
}