From 2819d0ea24961d8ff56051fbfb68fec81865c462 Mon Sep 17 00:00:00 2001 From: Dexterity <173429049+Dexterity104@users.noreply.github.com> Date: Tue, 2 Jun 2026 03:27:26 -0400 Subject: [PATCH] fix(go-models): use per call context timeouts so long streaming responses are not truncated (#15380) ### What problem does this PR solve? Closes #15379 Around 29 Go model providers in `internal/entity/models/` share an `http.Client` configured with `Timeout: 120 * time.Second`, and reuse that same client for `ChatStreamlyWithSender`. Go's `http.Client.Timeout` is a hard ceiling on the whole request that also covers reading the response body, so it behaves as a wall clock on streaming. Any streamed chat response that lasts longer than 120 seconds gets cut off in the middle with a timeout error. Long generations, reasoning model outputs, and slow or overloaded upstreams are the common victims. The providers that already behave correctly (`groq`, `mistral`, `voyage`, `anthropic`) set no client `Timeout` and instead wrap each request in a `context.WithTimeout`. This change converges the affected providers onto that same pattern. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --------- Co-authored-by: Jin Hai --- internal/entity/models/302ai.go | 44 ++++++-- internal/entity/models/aliyun.go | 23 +++- internal/entity/models/avian.go | 4 +- internal/entity/models/baichuan.go | 18 ++- internal/entity/models/baidu.go | 29 +++-- internal/entity/models/cohere.go | 48 +++++--- internal/entity/models/deepinfra.go | 48 ++++++-- internal/entity/models/deepseek.go | 22 +++- internal/entity/models/fishaudio.go | 43 ++++--- internal/entity/models/gitee.go | 77 +++++++++---- internal/entity/models/groq.go | 4 +- internal/entity/models/huaweicloud.go | 6 +- internal/entity/models/huggingface.go | 38 ++++--- internal/entity/models/lmstudio.go | 19 +++- internal/entity/models/mineru.go | 13 ++- internal/entity/models/mineru_local.go | 13 ++- internal/entity/models/minimax.go | 32 ++++-- internal/entity/models/moonshot.go | 22 +++- internal/entity/models/nvidia.go | 21 ++-- internal/entity/models/ollama.go | 19 +++- internal/entity/models/openrouter.go | 43 +++++-- internal/entity/models/orcarouter.go | 22 +++- internal/entity/models/paddleocr.go | 24 ++-- internal/entity/models/paddleocr_local.go | 8 +- internal/entity/models/perplexity.go | 4 +- internal/entity/models/ppio.go | 4 +- internal/entity/models/ppio_test.go | 9 -- internal/entity/models/qiniu.go | 17 ++- internal/entity/models/siliconflow.go | 47 ++++++-- internal/entity/models/timeout_test.go | 132 ++++++++++++++++++++++ internal/entity/models/togetherai.go | 4 +- internal/entity/models/tokenhub.go | 22 +++- internal/entity/models/vllm.go | 21 ++-- internal/entity/models/volcengine.go | 86 +++++++++----- internal/entity/models/xai.go | 33 +++++- internal/entity/models/xunfei.go | 18 ++- internal/entity/models/zhipu-ai.go | 52 +++++++-- web/src/locales/zh.ts | 6 +- 38 files changed, 805 insertions(+), 290 deletions(-) create mode 100644 internal/entity/models/timeout_test.go diff --git a/internal/entity/models/302ai.go b/internal/entity/models/302ai.go index dbdfcbec49..2195b76721 100644 --- a/internal/entity/models/302ai.go +++ b/internal/entity/models/302ai.go @@ -29,7 +29,6 @@ func NewAI302Model(baseURL map[string]string, urlSuffix URLSuffix) *AI302Model { BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, MaxIdleConns: 10, @@ -46,7 +45,6 @@ func (a *AI302Model) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: a.URLSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, MaxIdleConns: 10, @@ -162,7 +160,10 @@ func (a *AI302Model) ChatWithMessages(modelName string, messages []Message, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -316,7 +317,10 @@ func (a *AI302Model) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -435,7 +439,10 @@ func (a *AI302Model) Embed(modelName *string, texts []string, apiConfig *APIConf return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -524,7 +531,10 @@ func (a *AI302Model) Rerank(modelName *string, query string, documents []string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -654,7 +664,10 @@ func (a *AI302Model) TranscribeAudio(modelName *string, file *string, apiConfig } // build request - req, err := http.NewRequest("POST", url, &body) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, &body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -749,7 +762,7 @@ func (a *AI302Model) OCRFile(modelName *string, content []byte, urls *string, ap return nil, fmt.Errorf("failed to marshal json payload: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -832,7 +845,10 @@ func (a *AI302Model) ParseFile(modelName *string, content []byte, documentURL *s return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -881,7 +897,10 @@ func (a *AI302Model) ListModels(apiConfig *APIConfig) ([]string, error) { url := fmt.Sprintf("%s/%s", a.BaseURL[region], a.URLSuffix.Models) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -956,7 +975,10 @@ func (a *AI302Model) ShowTask(taskID string, apiConfig *APIConfig) (*TaskRespons // URL: https://mineru.net/api/v4/extract/task/{task_id} apiURL := fmt.Sprintf("%s/%s/%s", a.BaseURL[region], a.URLSuffix.DocumentParse, url.PathEscape(strings.TrimSpace(taskID))) - req, err := http.NewRequest("GET", apiURL, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/aliyun.go b/internal/entity/models/aliyun.go index 966be088d9..1e7cbdcb73 100644 --- a/internal/entity/models/aliyun.go +++ b/internal/entity/models/aliyun.go @@ -42,7 +42,6 @@ func NewAliyunModel(baseURL map[string]string, urlSuffix URLSuffix) *AliyunModel BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -130,7 +129,10 @@ func (z *AliyunModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -273,7 +275,10 @@ func (z *AliyunModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -422,7 +427,7 @@ func (z *AliyunModel) Embed(modelName *string, texts []string, apiConfig *APICon return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -525,7 +530,10 @@ func (z *AliyunModel) Rerank(modelName *string, query string, documents []string return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -622,7 +630,10 @@ func (z *AliyunModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/avian.go b/internal/entity/models/avian.go index 44e86a22b9..53edcdbafd 100644 --- a/internal/entity/models/avian.go +++ b/internal/entity/models/avian.go @@ -222,8 +222,6 @@ func (a *AvianModel) ChatWithMessages(modelName string, messages []Message, apiC }, nil } -const avianStreamTimeout = 10 * time.Minute - func (a *AvianModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { if sender == nil { return fmt.Errorf("sender is required") @@ -254,7 +252,7 @@ func (a *AvianModel) ChatStreamlyWithSender(modelName string, messages []Message // ResponseHeaderTimeout caps the initial header wait. This context // also caps the body-read phase so a stalled SSE stream cannot hold // the caller's goroutine and connection indefinitely. - ctx, cancel := context.WithTimeout(context.Background(), avianStreamTimeout) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) diff --git a/internal/entity/models/baichuan.go b/internal/entity/models/baichuan.go index a4744ea3fd..1e655ed30e 100644 --- a/internal/entity/models/baichuan.go +++ b/internal/entity/models/baichuan.go @@ -3,6 +3,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -24,7 +25,6 @@ func NewBaichuanModel(baseURL map[string]string, urlSuffix URLSuffix) *BaichuanM BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -40,7 +40,6 @@ func (b *BaichuanModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: b.URLSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -110,7 +109,10 @@ func (b *BaichuanModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -224,7 +226,10 @@ func (b *BaichuanModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -328,7 +333,10 @@ func (b *BaichuanModel) Embed(modelName *string, texts []string, apiConfig *APIC return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/baidu.go b/internal/entity/models/baidu.go index c1ed319875..6e5f661c8f 100644 --- a/internal/entity/models/baidu.go +++ b/internal/entity/models/baidu.go @@ -25,7 +25,6 @@ func (b *BaiduModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: b.URLSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -41,7 +40,6 @@ func NewBaiduModel(baseURL map[string]string, urlSuffix URLSuffix) *BaiduModel { BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxConnsPerHost: 10, MaxIdleConnsPerHost: 100, @@ -156,7 +154,10 @@ func (b *BaiduModel) ChatWithMessages(modelName string, messages []Message, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -326,7 +327,10 @@ func (b *BaiduModel) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -466,7 +470,10 @@ func (b *BaiduModel) Embed(modelName *string, texts []string, apiConfig *APIConf return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -559,7 +566,10 @@ func (b *BaiduModel) Rerank(modelName *string, query string, documents []string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -680,7 +690,7 @@ func (b *BaiduModel) OCRFile(modelName *string, content []byte, fileURL *string, return nil, fmt.Errorf("failed to marshal json payload: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -740,7 +750,10 @@ func (b *BaiduModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/cohere.go b/internal/entity/models/cohere.go index 0592d58b7d..ac79faea93 100644 --- a/internal/entity/models/cohere.go +++ b/internal/entity/models/cohere.go @@ -3,6 +3,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -12,7 +13,6 @@ import ( "path/filepath" "strconv" "strings" - "time" ) type CoHereModel struct { @@ -23,21 +23,17 @@ type CoHereModel struct { func (c *CoHereModel) NewInstance(baseURL map[string]string) ModelDriver { return &CoHereModel{ - BaseURL: baseURL, - URLSuffix: c.URLSuffix, - httpClient: &http.Client{ - Timeout: 120 * time.Second, - }, + BaseURL: baseURL, + URLSuffix: c.URLSuffix, + httpClient: &http.Client{}, } } func NewCoHereModel(baseURL map[string]string, urlSuffix URLSuffix) *CoHereModel { return &CoHereModel{ - BaseURL: baseURL, - URLSuffix: urlSuffix, - httpClient: &http.Client{ - Timeout: 120 * time.Second, - }, + BaseURL: baseURL, + URLSuffix: urlSuffix, + httpClient: &http.Client{}, } } @@ -112,7 +108,10 @@ func (c *CoHereModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -250,7 +249,10 @@ func (c *CoHereModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -359,7 +361,10 @@ func (c *CoHereModel) Embed(modelName *string, texts []string, apiConfig *APICon return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -438,7 +443,10 @@ func (c *CoHereModel) Rerank(modelName *string, query string, documents []string return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -559,7 +567,10 @@ func (c *CoHereModel) TranscribeAudio(modelName *string, file *string, apiConfig } // build request - req, err := http.NewRequest("POST", url, &body) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, &body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -624,7 +635,10 @@ func (c *CoHereModel) ListModels(apiConfig *APIConfig) ([]string, error) { url := fmt.Sprintf("%s/%s", c.BaseURL[region], c.URLSuffix.Models) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/deepinfra.go b/internal/entity/models/deepinfra.go index 75b00a363b..de3397dbcf 100644 --- a/internal/entity/models/deepinfra.go +++ b/internal/entity/models/deepinfra.go @@ -3,6 +3,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -29,7 +30,6 @@ func NewDeepInfraModel(baseURL map[string]string, urlSuffix URLSuffix) *DeepInfr BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -45,7 +45,6 @@ func (d *DeepInfraModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: d.URLSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -127,7 +126,10 @@ func (d *DeepInfraModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -261,7 +263,10 @@ func (d *DeepInfraModel) ChatStreamlyWithSender(modelName string, messages []Mes return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -376,7 +381,10 @@ func (d *DeepInfraModel) Embed(modelName *string, texts []string, apiConfig *API return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -470,7 +478,10 @@ func (d *DeepInfraModel) Rerank( return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -600,7 +611,10 @@ func (d *DeepInfraModel) TranscribeAudio(modelName *string, file *string, apiCon return nil, fmt.Errorf("failed to close multipart writer: %w", err) } - req, err := http.NewRequest("POST", url, &body) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, &body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -691,7 +705,10 @@ func (d *DeepInfraModel) AudioSpeech(modelName *string, audioContent *string, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -767,7 +784,10 @@ func (d *DeepInfraModel) AudioSpeechWithSender(modelName *string, audioContent * return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -836,7 +856,10 @@ func (d *DeepInfraModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -888,7 +911,10 @@ func (d *DeepInfraModel) Balance(apiConfig *APIConfig) (map[string]interface{}, url := fmt.Sprintf("%s/%s", d.BaseURL[region], d.URLSuffix.Balance) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/deepseek.go b/internal/entity/models/deepseek.go index 1afd0f3fad..b1cb810b0f 100644 --- a/internal/entity/models/deepseek.go +++ b/internal/entity/models/deepseek.go @@ -19,6 +19,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -42,7 +43,6 @@ func NewDeepSeekModel(baseURL map[string]string, urlSuffix URLSuffix) *DeepSeekM BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -157,7 +157,10 @@ func (z *DeepSeekModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -327,7 +330,10 @@ func (z *DeepSeekModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -448,7 +454,10 @@ func (z *DeepSeekModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -528,7 +537,10 @@ func (z *DeepSeekModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), z.URLSuffix.Balance) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/fishaudio.go b/internal/entity/models/fishaudio.go index 070769f828..f0c4d9e0dc 100644 --- a/internal/entity/models/fishaudio.go +++ b/internal/entity/models/fishaudio.go @@ -3,6 +3,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/base64" "encoding/json" "fmt" @@ -13,7 +14,6 @@ import ( "path/filepath" "strconv" "strings" - "time" ) // 208cc2d0e4594ca896a600c43c9497aa @@ -26,21 +26,17 @@ type FishAudioModel struct { func NewFishAudioModel(baseURL map[string]string, urlSuffix URLSuffix) *FishAudioModel { return &FishAudioModel{ - BaseURL: baseURL, - URLSuffix: urlSuffix, - httpClient: &http.Client{ - Timeout: 120 * time.Second, - }, + BaseURL: baseURL, + URLSuffix: urlSuffix, + httpClient: &http.Client{}, } } func (f *FishAudioModel) NewInstance(baseURL map[string]string) ModelDriver { return &FishAudioModel{ - BaseURL: baseURL, - URLSuffix: f.URLSuffix, - httpClient: &http.Client{ - Timeout: 120 * time.Second, - }, + BaseURL: baseURL, + URLSuffix: f.URLSuffix, + httpClient: &http.Client{}, } } @@ -130,7 +126,10 @@ func (f *FishAudioModel) TranscribeAudio(modelName *string, file *string, apiCon } // request - req, err := http.NewRequest("POST", url, &body) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, &body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -206,7 +205,10 @@ func (f *FishAudioModel) AudioSpeech(modelName *string, audioContent *string, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -268,7 +270,10 @@ func (f *FishAudioModel) AudioSpeechWithSender(modelName *string, audioContent * } // Build Request - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -348,7 +353,10 @@ func (f *FishAudioModel) ListModels(apiConfig *APIConfig) ([]string, error) { url := fmt.Sprintf("%s/%s", f.BaseURL[region], f.URLSuffix.Models) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -406,7 +414,10 @@ func (f *FishAudioModel) Balance(apiConfig *APIConfig) (map[string]interface{}, url := fmt.Sprintf("%s/wallet/self/api-credit", strings.TrimSuffix(baseURL, "/")) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/gitee.go b/internal/entity/models/gitee.go index 552165d1b3..22e7ccbf73 100644 --- a/internal/entity/models/gitee.go +++ b/internal/entity/models/gitee.go @@ -43,7 +43,6 @@ func NewGiteeModel(baseURL map[string]string, urlSuffix URLSuffix) *GiteeModel { BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -137,7 +136,10 @@ func (g *GiteeModel) ChatWithMessages(modelName string, messages []Message, apiC common.Info(fmt.Sprintf("GiteeAPI request body: %s", string(jsonData))) - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -284,7 +286,10 @@ func (g *GiteeModel) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -462,7 +467,7 @@ func (g *GiteeModel) Embed(modelName *string, texts []string, apiConfig *APIConf return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -561,7 +566,7 @@ func (g *GiteeModel) Rerank(modelName *string, query string, documents []string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -674,7 +679,7 @@ func (g *GiteeModel) OCRFile(modelName *string, content []byte, imageURL *string writer.Close() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, payload) @@ -780,7 +785,7 @@ func (g *GiteeModel) ParseFile(modelName *string, content []byte, documentURL *s writer.Close() - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, payload) @@ -834,24 +839,25 @@ func (g *GiteeModel) getParseFile(baseURL *string, apiKey, taskID *string, timeO return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey)) - - var resp *http.Response for i := 0; i < count; i++ { - resp, err = g.httpClient.Do(req) + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey)) + + resp, err := g.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } - defer resp.Body.Close() - var body []byte - body, err = io.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) + resp.Body.Close() if err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } @@ -866,7 +872,13 @@ func (g *GiteeModel) getParseFile(baseURL *string, apiKey, taskID *string, timeO return nil, fmt.Errorf("failed to parse response: %w", err) } - time.Sleep(timeOut) + // Wait before the next poll, but honor context cancellation / + // longOpCallTimeout instead of blocking uninterruptibly. + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(timeOut): + } } // if resp show the file is ok, download it. otherwise, provide timeout info @@ -889,7 +901,10 @@ func (g *GiteeModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -946,7 +961,10 @@ func (g *GiteeModel) Balance(apiConfig *APIConfig) (map[string]interface{}, erro return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -1001,7 +1019,10 @@ func (g *GiteeModel) CheckConnection(apiConfig *APIConfig) error { return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -1073,7 +1094,10 @@ func (g *GiteeModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -1128,7 +1152,10 @@ func (g *GiteeModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskRespons return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/groq.go b/internal/entity/models/groq.go index fdf3d0652d..50c106ad1c 100644 --- a/internal/entity/models/groq.go +++ b/internal/entity/models/groq.go @@ -232,8 +232,6 @@ func (g *GroqModel) ChatWithMessages(modelName string, messages []Message, apiCo }, nil } -const groqStreamTimeout = 10 * time.Minute - func (g *GroqModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { if sender == nil { return fmt.Errorf("sender is required") @@ -261,7 +259,7 @@ func (g *GroqModel) ChatStreamlyWithSender(modelName string, messages []Message, return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), groqStreamTimeout) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) diff --git a/internal/entity/models/huaweicloud.go b/internal/entity/models/huaweicloud.go index 56a9d06e57..1e977ca7ba 100644 --- a/internal/entity/models/huaweicloud.go +++ b/internal/entity/models/huaweicloud.go @@ -24,7 +24,6 @@ func NewHuaweiCloudModel(baseURL map[string]string, urlSuffix URLSuffix) *Huawei BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -303,7 +302,10 @@ func (h *HuaweiCloudModel) ChatStreamlyWithSender(modelName string, messages []M return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/huggingface.go b/internal/entity/models/huggingface.go index 9a63766588..76d1c1519a 100644 --- a/internal/entity/models/huggingface.go +++ b/internal/entity/models/huggingface.go @@ -3,13 +3,13 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" "net/http" "ragflow/internal/common" "strings" - "time" ) // HuggingFaceModel implements ModelDriver for HuggingFace @@ -22,20 +22,16 @@ type HuggingFaceModel struct { // NewHuggingFaceModel creates a new huggingFace model instance func NewHuggingFaceModel(baseURL map[string]string, urlSuffix URLSuffix) *HuggingFaceModel { return &HuggingFaceModel{ - BaseURL: baseURL, - URLSuffix: urlSuffix, - httpClient: &http.Client{ - Timeout: 120 * time.Second, - }, + BaseURL: baseURL, + URLSuffix: urlSuffix, + httpClient: &http.Client{}, } } func (h *HuggingFaceModel) NewInstance(baseURL map[string]string) ModelDriver { return &HuggingFaceModel{ - BaseURL: baseURL, - URLSuffix: h.URLSuffix, - httpClient: &http.Client{ - Timeout: 120 * time.Second, - }, + BaseURL: baseURL, + URLSuffix: h.URLSuffix, + httpClient: &http.Client{}, } } @@ -111,7 +107,10 @@ func (h *HuggingFaceModel) ChatWithMessages(modelName string, messages []Message return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -251,7 +250,10 @@ func (h *HuggingFaceModel) ChatStreamlyWithSender(modelName string, messages []M return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -369,7 +371,10 @@ func (h *HuggingFaceModel) Embed(modelName *string, texts []string, apiConfig *A url := fmt.Sprintf("%s/%s/%s", h.BaseURL[region], h.URLSuffix.Embedding, *modelName) - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, err } @@ -456,7 +461,10 @@ func (h *HuggingFaceModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/lmstudio.go b/internal/entity/models/lmstudio.go index 0ff2ee7b06..840f63de30 100644 --- a/internal/entity/models/lmstudio.go +++ b/internal/entity/models/lmstudio.go @@ -26,7 +26,6 @@ func NewLmStudioModel(baseURL map[string]string, urlSuffix URLSuffix) *LmStudioM BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -42,7 +41,6 @@ func (l *LmStudioModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: l.URLSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -132,7 +130,10 @@ func (l *LmStudioModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -274,7 +275,10 @@ func (l *LmStudioModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -400,7 +404,7 @@ func (l *LmStudioModel) Embed(modelName *string, texts []string, apiConfig *APIC return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -500,7 +504,10 @@ func (l *LmStudioModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/mineru.go b/internal/entity/models/mineru.go index b8a14be6b7..2a395b2600 100644 --- a/internal/entity/models/mineru.go +++ b/internal/entity/models/mineru.go @@ -2,6 +2,7 @@ package models import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -20,7 +21,6 @@ func NewMinerUModel(baseURL map[string]string, urlSuffix URLSuffix) *MinerUModel BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -36,7 +36,6 @@ func (m *MinerUModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: m.URLSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -137,7 +136,10 @@ func (m *MinerUModel) ParseFile(modelName *string, content []byte, documentURL * return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -206,7 +208,10 @@ func (m *MinerUModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskRespon // URL: https://mineru.net/api/v4/extract/task/{task_id} apiURL := fmt.Sprintf("%s/api/%s/%s", m.BaseURL[region], m.URLSuffix.DocumentParse, taskID) - req, err := http.NewRequest("GET", apiURL, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/mineru_local.go b/internal/entity/models/mineru_local.go index 4307f0e8b1..7fc5d6c53a 100644 --- a/internal/entity/models/mineru_local.go +++ b/internal/entity/models/mineru_local.go @@ -2,6 +2,7 @@ package models import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -21,7 +22,6 @@ func NewMinerLocalUModel(baseURL map[string]string, urlSuffix URLSuffix) *MinerU BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -37,7 +37,6 @@ func (m *MinerULocalModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: m.URLSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -134,7 +133,10 @@ func (m *MinerULocalModel) ParseFile(modelName *string, content []byte, document return nil, fmt.Errorf("failed to close multipart writer: %w", err) } - req, err := http.NewRequest("POST", apiURL, &body) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, &body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -198,7 +200,10 @@ func (m *MinerULocalModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskR } url := fmt.Sprintf("%s/%s/%s/result", m.BaseURL[region], m.URLSuffix.Task, taskID) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create status request: %w", err) } diff --git a/internal/entity/models/minimax.go b/internal/entity/models/minimax.go index 97b825cf6a..d05adb8285 100644 --- a/internal/entity/models/minimax.go +++ b/internal/entity/models/minimax.go @@ -19,6 +19,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/hex" "encoding/json" "fmt" @@ -42,7 +43,6 @@ func NewMinimaxModel(baseURL map[string]string, urlSuffix URLSuffix) *MinimaxMod BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -134,7 +134,10 @@ func (z *MinimaxModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -275,7 +278,10 @@ func (z *MinimaxModel) ChatStreamlyWithSender(modelName string, messages []Messa return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -385,7 +391,10 @@ func (z *MinimaxModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -437,7 +446,10 @@ func (z *MinimaxModel) CheckConnection(apiConfig *APIConfig) error { url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Files) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -514,7 +526,10 @@ func (z *MinimaxModel) AudioSpeech(modelName *string, audioContent *string, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -613,7 +628,10 @@ func (z *MinimaxModel) AudioSpeechWithSender(modelName *string, audioContent *st return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/moonshot.go b/internal/entity/models/moonshot.go index 4e48fd367a..9d17c6339e 100644 --- a/internal/entity/models/moonshot.go +++ b/internal/entity/models/moonshot.go @@ -19,6 +19,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -41,7 +42,6 @@ func NewMoonshotModel(baseURL map[string]string, urlSuffix URLSuffix) *MoonshotM BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -128,7 +128,10 @@ func (k *MoonshotModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -271,7 +274,10 @@ func (k *MoonshotModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -381,7 +387,10 @@ func (z *MoonshotModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -437,7 +446,10 @@ func (z *MoonshotModel) Balance(apiConfig *APIConfig) (map[string]interface{}, e return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/nvidia.go b/internal/entity/models/nvidia.go index c44e186723..d9e2601522 100644 --- a/internal/entity/models/nvidia.go +++ b/internal/entity/models/nvidia.go @@ -25,7 +25,6 @@ func NewNvidiaModel(baseURL map[string]string, urlSuffix URLSuffix) *NvidiaModel BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -41,7 +40,6 @@ func (n NvidiaModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: n.URLSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -116,7 +114,10 @@ func (n *NvidiaModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -248,7 +249,10 @@ func (n *NvidiaModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -382,7 +386,7 @@ func (n NvidiaModel) Embed(modelName *string, texts []string, apiConfig *APIConf return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -508,7 +512,7 @@ func (n NvidiaModel) Rerank(modelName *string, query string, documents []string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -601,7 +605,10 @@ func (n NvidiaModel) ListModels(apiConfig *APIConfig) ([]string, error) { url := fmt.Sprintf("%s/%s", baseURL, n.URLSuffix.Models) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/ollama.go b/internal/entity/models/ollama.go index fa7889c3bf..3068865d63 100644 --- a/internal/entity/models/ollama.go +++ b/internal/entity/models/ollama.go @@ -25,7 +25,6 @@ func NewOllamaModel(baseURL map[string]string, urlSuffix URLSuffix) *OllamaModel BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -41,7 +40,6 @@ func (o *OllamaModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: o.URLSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -134,7 +132,10 @@ func (o *OllamaModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -262,7 +263,10 @@ func (o *OllamaModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -361,7 +365,7 @@ func (o *OllamaModel) Embed(modelName *string, texts []string, apiConfig *APICon return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -473,7 +477,10 @@ func (o *OllamaModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/openrouter.go b/internal/entity/models/openrouter.go index 876e73f953..204de2bcb3 100644 --- a/internal/entity/models/openrouter.go +++ b/internal/entity/models/openrouter.go @@ -3,6 +3,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/base64" "encoding/json" "fmt" @@ -28,7 +29,6 @@ func NewOpenRouterModel(baseURL map[string]string, urlSuffix URLSuffix) *OpenRou BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -44,7 +44,6 @@ func (o *OpenRouterModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: o.URLSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -124,7 +123,10 @@ func (o *OpenRouterModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -268,7 +270,10 @@ func (o *OpenRouterModel) ChatStreamlyWithSender(modelName string, messages []Me return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -404,7 +409,10 @@ func (o *OpenRouterModel) Embed(modelName *string, texts []string, apiConfig *AP return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -495,7 +503,10 @@ func (o *OpenRouterModel) Rerank(modelName *string, query string, documents []st url := fmt.Sprintf("%s/%s", strings.TrimSuffix(o.BaseURL[region], "/"), o.URLSuffix.Rerank) - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -603,7 +614,10 @@ func (o *OpenRouterModel) TranscribeAudio(modelName *string, file *string, apiCo } url := fmt.Sprintf("%s/%s", strings.TrimSuffix(o.BaseURL[region], "/"), o.URLSuffix.ASR) - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -674,7 +688,10 @@ func (o *OpenRouterModel) AudioSpeech(modelName *string, audioContent *string, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -730,7 +747,10 @@ func (o *OpenRouterModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -778,7 +798,10 @@ func (o *OpenRouterModel) Balance(apiConfig *APIConfig) (map[string]interface{}, url := fmt.Sprintf("%s/%s", o.BaseURL[region], o.URLSuffix.Balance) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/orcarouter.go b/internal/entity/models/orcarouter.go index b414491e85..09a315f650 100644 --- a/internal/entity/models/orcarouter.go +++ b/internal/entity/models/orcarouter.go @@ -3,6 +3,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -23,7 +24,6 @@ func NewOrcaRouterModel(baseURL map[string]string, urlSuffix URLSuffix) *OrcaRou BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -101,7 +101,10 @@ func (o *OrcaRouterModel) ChatWithMessages(modelName string, messages []Message, return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -215,7 +218,10 @@ func (o *OrcaRouterModel) ChatStreamlyWithSender(modelName string, messages []Me return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -345,7 +351,10 @@ func (o *OrcaRouterModel) AudioSpeech(modelName *string, audioContent *string, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -398,7 +407,10 @@ func (o *OrcaRouterModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/paddleocr.go b/internal/entity/models/paddleocr.go index 1218d149a3..f9705638f7 100644 --- a/internal/entity/models/paddleocr.go +++ b/internal/entity/models/paddleocr.go @@ -3,6 +3,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -23,7 +24,6 @@ func NewPaddleOCRModel(baseURL map[string]string, urlSuffix URLSuffix) *PaddleOC BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -39,7 +39,6 @@ func (p PaddleOCRModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: p.URLSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -135,6 +134,11 @@ func (p *PaddleOCRModel) OCRFile(modelName *string, content []byte, fileURL *str } optBytes, _ := json.Marshal(optionalPayload) + // One generous deadline bounds the whole OCR operation (submit + poll + + // result download), so the poll loop below can no longer spin forever. + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + var req *http.Request var err error @@ -148,7 +152,7 @@ func (p *PaddleOCRModel) OCRFile(modelName *string, content []byte, fileURL *str if err != nil { return nil, fmt.Errorf("failed to marshal json: %w", err) } - req, err = http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + req, err = http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") } else { body := &bytes.Buffer{} @@ -164,7 +168,7 @@ func (p *PaddleOCRModel) OCRFile(modelName *string, content []byte, fileURL *str part.Write(content) writer.Close() - req, err = http.NewRequest("POST", url, body) + req, err = http.NewRequestWithContext(ctx, "POST", url, body) req.Header.Set("Content-Type", writer.FormDataContentType()) } @@ -195,9 +199,15 @@ func (p *PaddleOCRModel) OCRFile(modelName *string, content []byte, fileURL *str var jsonlUrl string for { - time.Sleep(3 * time.Second) + // Wait between polls but bail out immediately if the overall + // deadline fires instead of sleeping through it. + select { + case <-time.After(3 * time.Second): + case <-ctx.Done(): + return nil, ctx.Err() + } - pollReq, _ := http.NewRequest("GET", pollUrl, nil) + pollReq, _ := http.NewRequestWithContext(ctx, "GET", pollUrl, nil) pollReq.Header.Set("Authorization", fmt.Sprintf("bearer %s", *apiConfig.ApiKey)) pollResp, err := p.httpClient.Do(pollReq) @@ -231,7 +241,7 @@ func (p *PaddleOCRModel) OCRFile(modelName *string, content []byte, fileURL *str return nil, fmt.Errorf("job done but jsonl url is empty") } - resReq, err := http.NewRequest("GET", jsonlUrl, nil) + resReq, err := http.NewRequestWithContext(ctx, "GET", jsonlUrl, nil) if err != nil { return nil, fmt.Errorf("failed to create request for jsonl: %w", err) } diff --git a/internal/entity/models/paddleocr_local.go b/internal/entity/models/paddleocr_local.go index c562b4f9ac..c95c0b4778 100644 --- a/internal/entity/models/paddleocr_local.go +++ b/internal/entity/models/paddleocr_local.go @@ -2,6 +2,7 @@ package models import ( "bytes" + "context" "encoding/base64" "encoding/json" "fmt" @@ -22,7 +23,6 @@ func NewPaddleOCRLocalModel(baseURL map[string]string, urlSuffix URLSuffix) *Pad BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -38,7 +38,6 @@ func (p *PaddleOCRLocalModel) NewInstance(baseURL map[string]string) ModelDriver BaseURL: baseURL, URLSuffix: p.URLSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -134,7 +133,10 @@ func (p *PaddleOCRLocalModel) OCRFile(modelName *string, content []byte, fileURL return nil, fmt.Errorf("failed to marshal local PaddleOCR request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/perplexity.go b/internal/entity/models/perplexity.go index 921d12d4c3..a8c4205504 100644 --- a/internal/entity/models/perplexity.go +++ b/internal/entity/models/perplexity.go @@ -202,8 +202,6 @@ func (p *PerplexityModel) ChatWithMessages(modelName string, messages []Message, }, nil } -const perplexityStreamTimeout = 10 * time.Minute - func (p *PerplexityModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { if sender == nil { return fmt.Errorf("sender is required") @@ -234,7 +232,7 @@ func (p *PerplexityModel) ChatStreamlyWithSender(modelName string, messages []Me // ResponseHeaderTimeout caps the initial header wait. This context // also caps the body-read phase so a stalled SSE stream cannot hold // the caller's goroutine and connection indefinitely. - ctx, cancel := context.WithTimeout(context.Background(), perplexityStreamTimeout) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) diff --git a/internal/entity/models/ppio.go b/internal/entity/models/ppio.go index 8172951dbc..e9bcc33008 100644 --- a/internal/entity/models/ppio.go +++ b/internal/entity/models/ppio.go @@ -215,8 +215,6 @@ func (p *PPIOModel) ChatWithMessages(modelName string, messages []Message, apiCo }, nil } -const ppioStreamTimeout = 10 * time.Minute - func (p *PPIOModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { if sender == nil { return fmt.Errorf("sender is required") @@ -244,7 +242,7 @@ func (p *PPIOModel) ChatStreamlyWithSender(modelName string, messages []Message, return fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), ppioStreamTimeout) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) diff --git a/internal/entity/models/ppio_test.go b/internal/entity/models/ppio_test.go index 590ab69d11..54800fdd26 100644 --- a/internal/entity/models/ppio_test.go +++ b/internal/entity/models/ppio_test.go @@ -9,15 +9,6 @@ import ( "testing" ) -// roundTripperFunc is the package-wide test helper for stubbing -// http.RoundTripper. It lives here (the first provider test to need it) and is -// shared by the other provider tests in this package; do not redeclare it. -type roundTripperFunc func(*http.Request) (*http.Response, error) - -func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { - return f(r) -} - func newPPIOServer(t *testing.T, handler func(t *testing.T, r *http.Request, body map[string]interface{}, w http.ResponseWriter)) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/entity/models/qiniu.go b/internal/entity/models/qiniu.go index 0fea9cd33b..c7ee144a06 100644 --- a/internal/entity/models/qiniu.go +++ b/internal/entity/models/qiniu.go @@ -3,6 +3,7 @@ package models import ( "bufio" "bytes" + "context" "fmt" "io" "net/http" @@ -24,7 +25,6 @@ func NewQiniuModel(baseURL map[string]string, urlSuffix URLSuffix) *QiniuModel { BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxConnsPerHost: 10, MaxIdleConnsPerHost: 100, @@ -186,7 +186,10 @@ func (q *QiniuModel) ChatWithMessages(modelName string, messages []Message, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -310,7 +313,10 @@ func (q *QiniuModel) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -441,7 +447,10 @@ func (q *QiniuModel) ListModels(apiConfig *APIConfig) ([]string, error) { } url := fmt.Sprintf("%s/%s", baseURL, q.URLSuffix.Models) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/siliconflow.go b/internal/entity/models/siliconflow.go index 3764d4cf77..dc70a5d91b 100644 --- a/internal/entity/models/siliconflow.go +++ b/internal/entity/models/siliconflow.go @@ -19,6 +19,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -45,7 +46,6 @@ func NewSiliconflowModel(baseURL map[string]string, urlSuffix URLSuffix) *Silico BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -135,7 +135,10 @@ func (z *SiliconflowModel) ChatWithMessages(modelName string, messages []Message return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -283,7 +286,10 @@ func (z *SiliconflowModel) ChatStreamlyWithSender(modelName string, messages []M return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -430,7 +436,10 @@ func (s *SiliconflowModel) Embed(modelName *string, texts []string, apiConfig *A return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -488,7 +497,10 @@ func (z *SiliconflowModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -561,7 +573,10 @@ func (z *SiliconflowModel) Balance(apiConfig *APIConfig) (map[string]interface{} url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), z.URLSuffix.Balance) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -687,7 +702,10 @@ func (s *SiliconflowModel) Rerank(modelName *string, query string, documents []s url := fmt.Sprintf("%s/%s", strings.TrimSuffix(s.BaseURL[region], "/"), s.URLSuffix.Rerank) - req, err := http.NewRequest("POST", url, strings.NewReader(string(jsonData))) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(jsonData))) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -802,7 +820,10 @@ func (o *SiliconflowModel) TranscribeAudio(modelName *string, file *string, apiC } // build request - req, err := http.NewRequest("POST", url, &body) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, &body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -876,7 +897,10 @@ func (o *SiliconflowModel) AudioSpeech(modelName *string, audioContent *string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -938,7 +962,10 @@ func (z *SiliconflowModel) AudioSpeechWithSender(modelName *string, audioContent return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/timeout_test.go b/internal/entity/models/timeout_test.go new file mode 100644 index 0000000000..206bb34055 --- /dev/null +++ b/internal/entity/models/timeout_test.go @@ -0,0 +1,132 @@ +// +// Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package models + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +// withTestTimeouts temporarily shrinks the shared timeout knobs so deadline +// behaviour can be exercised with millisecond-scale delays instead of the +// production minutes/seconds, and restores them when the test ends. +func withTestTimeouts(t *testing.T, nonStream, stream time.Duration) { + t.Helper() + origNon, origStream := nonStreamCallTimeout, streamCallTimeout + nonStreamCallTimeout = nonStream + streamCallTimeout = stream + t.Cleanup(func() { + nonStreamCallTimeout = origNon + streamCallTimeout = origStream + }) +} + +func newTimeoutTestGroq(baseURL string) *GroqModel { + return NewGroqModel( + map[string]string{"default": baseURL}, + URLSuffix{Chat: "chat/completions", Models: "models"}, + ) +} + +// TestStreamNotTruncatedByNonStreamTimeout is the regression guard for the +// original bug: a streaming chat response that keeps emitting data for longer +// than the short non-streaming deadline must not be severed mid-stream. The +// stream is bounded by the generous streamCallTimeout, so a server that +// dribbles SSE events over a span well past nonStreamCallTimeout still +// completes intact. If a provider regressed to wrapping its stream in +// nonStreamCallTimeout (the old 120s-wall footgun, just relocated), the +// context would cancel mid-stream and this test would fail. +func TestStreamNotTruncatedByNonStreamTimeout(t *testing.T) { + // Stream emits for ~240ms, far past the 60ms non-stream deadline but well + // inside the 10s stream deadline. + withTestTimeouts(t, 60*time.Millisecond, 10*time.Second) + + chunks := []string{"Hello", " ", "streamed", " ", "world"} + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + flusher, ok := w.(http.Flusher) + if !ok { + t.Errorf("ResponseWriter does not support flushing") + return + } + w.Header().Set("Content-Type", "text/event-stream") + w.WriteHeader(http.StatusOK) + for _, c := range chunks { + time.Sleep(40 * time.Millisecond) + fmt.Fprintf(w, "data: {\"choices\":[{\"delta\":{\"content\":%q}}]}\n", c) + flusher.Flush() + } + time.Sleep(40 * time.Millisecond) + _, _ = io.WriteString(w, "data: [DONE]\n") + flusher.Flush() + })) + defer srv.Close() + + apiKey := "test-key" + var got strings.Builder + err := newTimeoutTestGroq(srv.URL).ChatStreamlyWithSender( + "llama-3.3-70b-versatile", + []Message{{Role: "user", Content: "hi"}}, + &APIConfig{ApiKey: &apiKey}, + nil, + func(c *string, _ *string) error { + if c != nil && *c != "[DONE]" { + got.WriteString(*c) + } + return nil + }, + ) + if err != nil { + t.Fatalf("streaming returned error (stream was truncated): %v", err) + } + if want := "Hello streamed world"; got.String() != want { + t.Fatalf("streamed content=%q, want %q", got.String(), want) + } +} + +// TestNonStreamHonorsShortDeadline ensures dropping the blanket +// http.Client.Timeout did not leave non-streaming calls unbounded: a slow +// server must still trip the per-call nonStreamCallTimeout promptly. +func TestNonStreamHonorsShortDeadline(t *testing.T) { + withTestTimeouts(t, 100*time.Millisecond, 10*time.Second) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(800 * time.Millisecond) // far beyond the 100ms non-stream deadline + _, _ = io.WriteString(w, `{"choices":[{"message":{"content":"late"}}]}`) + })) + defer srv.Close() + + apiKey := "test-key" + start := time.Now() + _, err := newTimeoutTestGroq(srv.URL).ChatWithMessages( + "llama-3.3-70b-versatile", + []Message{{Role: "user", Content: "hi"}}, + &APIConfig{ApiKey: &apiKey}, + nil, + ) + elapsed := time.Since(start) + if err == nil { + t.Fatal("expected a deadline error from the slow server, got nil") + } + if elapsed > 400*time.Millisecond { + t.Fatalf("call took %v; expected it to abort near the 100ms deadline", elapsed) + } +} diff --git a/internal/entity/models/togetherai.go b/internal/entity/models/togetherai.go index 35d2a65016..a112a970fa 100644 --- a/internal/entity/models/togetherai.go +++ b/internal/entity/models/togetherai.go @@ -215,8 +215,6 @@ func (t *TogetherAIModel) ChatWithMessages(modelName string, messages []Message, }, nil } -const togetherAIStreamTimeout = 10 * time.Minute - func (t *TogetherAIModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, sender func(*string, *string) error) error { if sender == nil { return fmt.Errorf("sender is required") @@ -247,7 +245,7 @@ func (t *TogetherAIModel) ChatStreamlyWithSender(modelName string, messages []Me // ResponseHeaderTimeout caps the initial header wait. This context // also caps the body-read phase so a stalled SSE stream cannot hold // the caller's goroutine and connection indefinitely. - ctx, cancel := context.WithTimeout(context.Background(), togetherAIStreamTimeout) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) diff --git a/internal/entity/models/tokenhub.go b/internal/entity/models/tokenhub.go index eb59cfa5b3..71cd1a763a 100644 --- a/internal/entity/models/tokenhub.go +++ b/internal/entity/models/tokenhub.go @@ -3,6 +3,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -22,7 +23,6 @@ func NewTokenHubModel(baseURL map[string]string, urlSuffix URLSuffix) *TokenHubM BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -106,7 +106,10 @@ func (t *TokenHubModel) ChatWithMessages(modelName string, messages []Message, a return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -238,7 +241,10 @@ func (t *TokenHubModel) ChatStreamlyWithSender(modelName string, messages []Mess return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -358,7 +364,10 @@ func (t *TokenHubModel) Embed(modelName *string, texts []string, apiConfig *APIC return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -447,7 +456,10 @@ func (t *TokenHubModel) ListModels(apiConfig *APIConfig) ([]string, error) { url := fmt.Sprintf("%s/%s", t.BaseURL[region], t.URLSuffix.Models) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/vllm.go b/internal/entity/models/vllm.go index 7857ad315a..bfa26eef79 100644 --- a/internal/entity/models/vllm.go +++ b/internal/entity/models/vllm.go @@ -42,7 +42,6 @@ func NewVllmModel(baseURL map[string]string, urlSuffix URLSuffix) *VllmModel { BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -58,7 +57,6 @@ func (z *VllmModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: z.URLSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -148,7 +146,10 @@ func (z *VllmModel) ChatWithMessages(modelName string, messages []Message, apiCo return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -290,7 +291,10 @@ func (z *VllmModel) ChatStreamlyWithSender(modelName string, messages []Message, return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -425,7 +429,7 @@ func (z *VllmModel) Embed(modelName *string, texts []string, apiConfig *APIConfi return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) @@ -493,7 +497,10 @@ func (z *VllmModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -617,7 +624,7 @@ func (z *VllmModel) Rerank(modelName *string, query string, documents []string, return nil, fmt.Errorf("failed to marshal request: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) diff --git a/internal/entity/models/volcengine.go b/internal/entity/models/volcengine.go index 165dadc821..ca302b4321 100644 --- a/internal/entity/models/volcengine.go +++ b/internal/entity/models/volcengine.go @@ -19,6 +19,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -41,7 +42,6 @@ func NewVolcEngine(baseURL map[string]string, urlSuffix URLSuffix) *VolcEngine { BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -150,7 +150,10 @@ func (z *VolcEngine) ChatWithMessages(modelName string, messages []Message, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -318,7 +321,10 @@ func (z *VolcEngine) ChatStreamlyWithSender(modelName string, messages []Message return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -468,33 +474,45 @@ func (z *VolcEngine) Embed(modelName *string, texts []string, apiConfig *APIConf ) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + // Run each per-text request in its own scope so the context's + // deadline is cancelled at the end of every iteration instead of + // piling up deferred cancels until the whole batch finishes. + parsed, err := func() (volcengineEmbeddingResponse, error) { + var parsed volcengineEmbeddingResponse + + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return parsed, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + + resp, err := z.httpClient.Do(req) + if err != nil { + return parsed, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return parsed, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return parsed, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + } + + if err = json.Unmarshal(body, &parsed); err != nil { + return parsed, fmt.Errorf("failed to parse response: %w", err) + } + return parsed, nil + }() if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) - - resp, err := z.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to send request: %w", err) - } - - body, err := io.ReadAll(resp.Body) - resp.Body.Close() - - if err != nil { - return nil, fmt.Errorf("failed to read response: %w", err) - } - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) - } - - var parsed volcengineEmbeddingResponse - if err = json.Unmarshal(body, &parsed); err != nil { - return nil, fmt.Errorf("failed to parse response: %w", err) + return nil, err } var embeddingData EmbeddingData @@ -559,7 +577,10 @@ func (z *VolcEngine) ListModels(apiConfig *APIConfig) ([]string, error) { url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), modelsSuffix) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -612,7 +633,10 @@ func (z *VolcEngine) CheckConnection(apiConfig *APIConfig) error { url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Files) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/xai.go b/internal/entity/models/xai.go index 67dc86b214..a30bd02c2d 100644 --- a/internal/entity/models/xai.go +++ b/internal/entity/models/xai.go @@ -32,11 +32,29 @@ import ( "time" ) -// nonStreamCallTimeout caps the time spent on a single non-streaming -// request (ChatWithMessages, ListModels). The shared httpClient itself -// has no client-wide timeout, so streaming requests can run as long as -// the API keeps the SSE connection open. -const nonStreamCallTimeout = 120 * time.Second +// Per-call context deadlines shared by every provider in this package. +// +// The shared httpClient sets no Client.Timeout: that field also bounds the time +// spent reading the response body, which would sever long-lived SSE streams in +// ChatStreamlyWithSender once a generation outlasts the limit. Each call wraps +// its request in a context.WithTimeout sized to the operation instead: +// +// - nonStreamCallTimeout for interactive non-streaming calls +// (chat, embed, rerank, list models, check connection, balance). +// - streamCallTimeout for streaming chat, bounded generously so slow or +// reasoning-heavy generations are not truncated mid-stream. +// - longOpCallTimeout for heavy synchronous file work (OCR, document +// parsing, audio transcription/synthesis) that legitimately runs for +// minutes. Kept distinct from streamCallTimeout so the two can be tuned +// independently even though they currently share a value. +// +// They are vars rather than consts so tests can shrink them to milliseconds +// and exercise the deadline behaviour without real-time waits. +var ( + nonStreamCallTimeout = 120 * time.Second + streamCallTimeout = 10 * time.Minute + longOpCallTimeout = 10 * time.Minute +) // XAIModel implements ModelDriver for xAI (Grok models) type XAIModel struct { @@ -296,7 +314,10 @@ func (z *XAIModel) ChatStreamlyWithSender(modelName string, messages []Message, return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/xunfei.go b/internal/entity/models/xunfei.go index 6d92c73993..dfd3234d71 100644 --- a/internal/entity/models/xunfei.go +++ b/internal/entity/models/xunfei.go @@ -3,6 +3,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -23,7 +24,6 @@ func NewXunFeiModel(baseURL map[string]string, urlSuffix URLSuffix) *XunFeiModel BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -39,7 +39,6 @@ func (x *XunFeiModel) NewInstance(baseURL map[string]string) ModelDriver { BaseURL: baseURL, URLSuffix: x.URLSuffix, httpClient: &http.Client{ - Timeout: time.Second * 120, Transport: &http.Transport{ MaxIdleConns: 10, MaxIdleConnsPerHost: 100, @@ -116,7 +115,10 @@ func (x *XunFeiModel) ChatWithMessages(modelName string, messages []Message, api return nil, fmt.Errorf("failed to marshal request body: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -247,7 +249,10 @@ func (x *XunFeiModel) ChatStreamlyWithSender(modelName string, messages []Messag return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -392,7 +397,10 @@ func (x *XunFeiModel) ListModels(apiConfig *APIConfig) ([]string, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("GET", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/internal/entity/models/zhipu-ai.go b/internal/entity/models/zhipu-ai.go index 50dd7f1b7a..f525ccf3a9 100644 --- a/internal/entity/models/zhipu-ai.go +++ b/internal/entity/models/zhipu-ai.go @@ -19,6 +19,7 @@ package models import ( "bufio" "bytes" + "context" "encoding/base64" "encoding/json" "fmt" @@ -45,7 +46,6 @@ func NewZhipuAIModel(baseURL map[string]string, urlSuffix URLSuffix) *ZhipuAIMod BaseURL: baseURL, URLSuffix: urlSuffix, httpClient: &http.Client{ - Timeout: 120 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, @@ -136,7 +136,10 @@ func (z *ZhipuAIModel) ChatWithMessages(modelName string, messages []Message, ap return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -278,7 +281,10 @@ func (z *ZhipuAIModel) ChatStreamlyWithSender(modelName string, messages []Messa return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -419,7 +425,10 @@ func (z *ZhipuAIModel) Embed(modelName *string, texts []string, apiConfig *APICo return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -478,7 +487,10 @@ func (z *ZhipuAIModel) ListModels(apiConfig *APIConfig) ([]string, error) { url := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), z.URLSuffix.Models) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -532,7 +544,10 @@ func (z *ZhipuAIModel) CheckConnection(apiConfig *APIConfig) error { url := fmt.Sprintf("%s/%s", z.BaseURL[region], z.URLSuffix.Files) - req, err := http.NewRequest("GET", url, nil) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -636,7 +651,10 @@ func (z *ZhipuAIModel) Rerank(modelName *string, query string, documents []strin return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), nonStreamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -730,7 +748,10 @@ func (z *ZhipuAIModel) TranscribeAudio(modelName *string, file *string, apiConfi return nil, fmt.Errorf("failed to close multipart writer: %w", err) } - req, err := http.NewRequest(http.MethodPost, url, &body) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -818,7 +839,10 @@ func (z *ZhipuAIModel) AudioSpeech(modelName *string, audioContent *string, apiC return nil, fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -856,7 +880,10 @@ func (z *ZhipuAIModel) AudioSpeechWithSender(modelName *string, audioContent *st return fmt.Errorf("failed to marshal request: %w", err) } - req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), streamCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -988,7 +1015,10 @@ func (z *ZhipuAIModel) OCRFile(modelName *string, content []byte, fileURL *strin } endpoint := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), strings.TrimPrefix(z.URLSuffix.OCR, "/")) - req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(jsonData)) + ctx, cancel := context.WithTimeout(context.Background(), longOpCallTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index ae2daf6d13..78f570cf4b 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1101,8 +1101,10 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系 dropboxDescription: '连接 Dropbox,同步指定账号下的文件与文件夹。', onedriveDescription: '连接 OneDrive 或 OneDrive for Business,通过 Microsoft Graph delta 查询索引文件和文件夹。', - onedriveTenantIdTip: 'Microsoft 365 组织的 Azure Active Directory 租户 ID(目录 ID)。', - onedriveClientIdTip: '拥有 Files.Read.All 权限的 Azure AD 应用注册的应用程序(客户端)ID。', + onedriveTenantIdTip: + 'Microsoft 365 组织的 Azure Active Directory 租户 ID(目录 ID)。', + onedriveClientIdTip: + '拥有 Files.Read.All 权限的 Azure AD 应用注册的应用程序(客户端)ID。', onedriveClientSecretTip: '在 Azure AD 应用注册中生成的客户端密钥值。', onedriveFolderPathTip: '可选的子文件夹路径,用于限制索引范围(例如 /Documents/Reports)。留空则索引整个云盘。',