mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 06:17:41 +00:00
feat(providers): make provider request timeout configurable (default 30s) (#1400)
The provider verify call and the provider models-list call used hardcoded timeouts (30s and 15s) that were too low for slow models. Introduce a tenant-scoped, UI-editable setting `providers.request_timeout_sec` (default 30) stored in system_configs, consumed by both handlers, and overlaid from config.json like tts.timeout_ms. Adds a field to the System Settings modal (all 4 locales) so operators can raise it without a redeploy. Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
This commit is contained in:
1 parent
951acad714
commit
d856a58218
13 files changed
+122
-3
No files matched your search
@@ -127,6 +127,9 @@ func seedConfigForContext(ctx context.Context, sc store.SystemConfigStore, cfg *
|
||||
setInt("tools.browser.max_pages", cfg.Tools.Browser.MaxPages)
|
||||
set("tools.browser.cookie_sync_enabled", fmt.Sprintf("%t", cfg.Tools.Browser.CookieSyncEnabled))
|
||||
|
||||
// Providers
|
||||
setInt("providers.request_timeout_sec", cfg.Providers.RequestTimeoutSec)
|
||||
|
||||
// TTS
|
||||
set("tts.provider", cfg.Tts.Provider)
|
||||
set("tts.auto", cfg.Tts.Auto)
|
||||
|
||||
@@ -276,6 +276,10 @@ type ProvidersConfig struct {
|
||||
BytePlus ProviderConfig `json:"byteplus"` // BytePlus ModelArk (Seed 2.0)
|
||||
BytePlusCoding ProviderConfig `json:"byteplus_coding"` // BytePlus ModelArk Coding Plan
|
||||
Vertex VertexConfig `json:"vertex"` // Google Cloud Vertex AI (OAuth2 service account + ADC)
|
||||
|
||||
// RequestTimeoutSec bounds provider verify and models-list HTTP calls.
|
||||
// Tenant-scoped, overridable via the "providers.request_timeout_sec" system config.
|
||||
RequestTimeoutSec int `json:"request_timeout_sec,omitempty"`
|
||||
}
|
||||
|
||||
// VertexConfig configures Google Cloud Vertex AI.
|
||||
|
||||
@@ -86,6 +86,9 @@ func (c *Config) ApplySystemConfigs(configs map[string]string) {
|
||||
boolValue(SkillSlashPartialMatchingSystemConfigKey, &c.Skills.SlashCommands.PartialMatching)
|
||||
str(SkillSlashCommandPrefixSystemConfigKey, &c.Skills.SlashCommands.Prefix)
|
||||
|
||||
// Providers
|
||||
integer("providers.request_timeout_sec", &c.Providers.RequestTimeoutSec)
|
||||
|
||||
// TTS
|
||||
str("tts.provider", &c.Tts.Provider)
|
||||
str("tts.auto", &c.Tts.Auto)
|
||||
|
||||
@@ -70,7 +70,7 @@ func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *ht
|
||||
// Ollama: use native /api/tags for richer metadata (parameter size, quantization, family).
|
||||
// ProviderOllama has no API key; ProviderOllamaCloud requires one but both use the same endpoint.
|
||||
if p.ProviderType == store.ProviderOllama || p.ProviderType == store.ProviderOllamaCloud {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(loadProviderRequestTimeoutSec(r.Context(), h.sysConfigStore))*time.Second)
|
||||
defer cancel()
|
||||
apiBase := h.resolveAPIBase(p)
|
||||
if apiBase == "" {
|
||||
@@ -91,7 +91,7 @@ func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(loadProviderRequestTimeoutSec(r.Context(), h.sysConfigStore))*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var models []ModelInfo
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
// defaultProviderRequestTimeoutSec is used when the tenant has not configured
|
||||
// (or has configured invalid values for) providers.request_timeout_sec.
|
||||
const defaultProviderRequestTimeoutSec = 30
|
||||
|
||||
// loadProviderRequestTimeoutSec reads the tenant's providers.request_timeout_sec
|
||||
// system config. Returns defaultProviderRequestTimeoutSec (30) when unset/invalid.
|
||||
func loadProviderRequestTimeoutSec(ctx context.Context, sc store.SystemConfigStore) int {
|
||||
if sc == nil {
|
||||
return defaultProviderRequestTimeoutSec
|
||||
}
|
||||
raw, err := sc.Get(ctx, "providers.request_timeout_sec")
|
||||
if err != nil || raw == "" {
|
||||
return defaultProviderRequestTimeoutSec
|
||||
}
|
||||
timeoutSec, err := strconv.Atoi(raw)
|
||||
if err != nil || timeoutSec <= 0 {
|
||||
return defaultProviderRequestTimeoutSec
|
||||
}
|
||||
return timeoutSec
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// stubProviderTimeoutStore returns a configured value for
|
||||
// "providers.request_timeout_sec" and ignores everything else.
|
||||
type stubProviderTimeoutStore struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func (s *stubProviderTimeoutStore) Get(_ context.Context, key string) (string, error) {
|
||||
if key == "providers.request_timeout_sec" {
|
||||
return s.value, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
func (s *stubProviderTimeoutStore) Set(_ context.Context, _, _ string) error { return nil }
|
||||
func (s *stubProviderTimeoutStore) Delete(_ context.Context, _ string) error { return nil }
|
||||
func (s *stubProviderTimeoutStore) List(_ context.Context) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
func TestLoadProviderRequestTimeoutSec(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
store *stubProviderTimeoutStore
|
||||
want int
|
||||
}{
|
||||
{name: "unset falls back to default", store: &stubProviderTimeoutStore{value: ""}, want: defaultProviderRequestTimeoutSec},
|
||||
{name: "valid value parsed", store: &stubProviderTimeoutStore{value: "45"}, want: 45},
|
||||
{name: "invalid non-numeric falls back to default", store: &stubProviderTimeoutStore{value: "abc"}, want: defaultProviderRequestTimeoutSec},
|
||||
{name: "zero falls back to default", store: &stubProviderTimeoutStore{value: "0"}, want: defaultProviderRequestTimeoutSec},
|
||||
{name: "negative falls back to default", store: &stubProviderTimeoutStore{value: "-5"}, want: defaultProviderRequestTimeoutSec},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := loadProviderRequestTimeoutSec(context.Background(), tt.store)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadProviderRequestTimeoutSec_NilStore(t *testing.T) {
|
||||
got := loadProviderRequestTimeoutSec(context.Background(), nil)
|
||||
assert.Equal(t, defaultProviderRequestTimeoutSec, got)
|
||||
}
|
||||
@@ -125,7 +125,8 @@ func (h *ProvidersHandler) handleVerifyProvider(w http.ResponseWriter, r *http.R
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
timeoutSec := loadProviderRequestTimeoutSec(r.Context(), h.sysConfigStore)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeoutSec)*time.Second)
|
||||
defer cancel()
|
||||
ctx = store.WithTenantID(ctx, p.TenantID)
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface InitState {
|
||||
kgMinConfidence: string;
|
||||
bgProvider: string;
|
||||
bgModel: string;
|
||||
providerRequestTimeoutSec: string;
|
||||
skillUploadMaxSize: string;
|
||||
skillSlashEnabled: boolean;
|
||||
skillSlashSuggest: boolean;
|
||||
@@ -59,6 +60,7 @@ export const DEFAULTS: InitState = {
|
||||
compThreshold: "", compKeepRecent: "", compMaxTokens: "",
|
||||
kgProvider: "", kgModel: "", kgMinConfidence: "0.75",
|
||||
bgProvider: "", bgModel: "",
|
||||
providerRequestTimeoutSec: "",
|
||||
skillUploadMaxSize: "20",
|
||||
skillSlashEnabled: true,
|
||||
skillSlashSuggest: true,
|
||||
|
||||
@@ -59,6 +59,7 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
|
||||
// Background Workers
|
||||
const [bgProvider, setBgProvider] = useState("");
|
||||
const [bgModel, setBgModel] = useState("");
|
||||
const [providerRequestTimeoutSec, setProviderRequestTimeoutSec] = useState("");
|
||||
const [skillUploadMaxSize, setSkillUploadMaxSize] = useState("20");
|
||||
const [skillSlashEnabled, setSkillSlashEnabled] = useState(true);
|
||||
const [skillSlashSuggest, setSkillSlashSuggest] = useState(true);
|
||||
@@ -80,6 +81,7 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
|
||||
kgProvider: kgSettings?.extraction_provider ?? "", kgModel: kgSettings?.extraction_model ?? "",
|
||||
kgMinConfidence: String(kgSettings?.min_confidence ?? 0.75),
|
||||
bgProvider: configs["background.provider"] ?? "", bgModel: configs["background.model"] ?? "",
|
||||
providerRequestTimeoutSec: configs["providers.request_timeout_sec"] ?? "",
|
||||
skillUploadMaxSize: configs["skills.max_upload_size_mb"] ?? "20",
|
||||
skillSlashEnabled: parseBool(configs["skills.slash_commands.enabled"], true),
|
||||
skillSlashSuggest: parseBool(configs["skills.slash_commands.suggest_not_found"], true),
|
||||
@@ -93,6 +95,7 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
|
||||
setCompProvider(s.compProvider); setCompModel(s.compModel); setCompThreshold(s.compThreshold); setCompKeepRecent(s.compKeepRecent); setCompMaxTokens(s.compMaxTokens);
|
||||
setKgProvider(s.kgProvider); setKgModel(s.kgModel); setKgMinConfidence(s.kgMinConfidence);
|
||||
setBgProvider(s.bgProvider); setBgModel(s.bgModel);
|
||||
setProviderRequestTimeoutSec(s.providerRequestTimeoutSec);
|
||||
setSkillUploadMaxSize(s.skillUploadMaxSize);
|
||||
setSkillSlashEnabled(s.skillSlashEnabled);
|
||||
setSkillSlashSuggest(s.skillSlashSuggest);
|
||||
@@ -140,6 +143,7 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
|
||||
if (compMaxTokens !== init.compMaxTokens) updates["compaction.max_tokens"] = compMaxTokens;
|
||||
if (bgProvider !== init.bgProvider) updates["background.provider"] = bgProvider;
|
||||
if (bgModel !== init.bgModel) updates["background.model"] = bgModel;
|
||||
if (providerRequestTimeoutSec !== init.providerRequestTimeoutSec) updates["providers.request_timeout_sec"] = providerRequestTimeoutSec.trim();
|
||||
if (skillUploadMaxSize !== init.skillUploadMaxSize) updates["skills.max_upload_size_mb"] = skillUploadMaxSize;
|
||||
if (skillSlashEnabled !== init.skillSlashEnabled) updates["skills.slash_commands.enabled"] = String(skillSlashEnabled);
|
||||
if (skillSlashSuggest !== init.skillSlashSuggest) updates["skills.slash_commands.suggest_not_found"] = String(skillSlashSuggest);
|
||||
@@ -213,6 +217,11 @@ export function SystemSettingsModal({ open, onOpenChange }: SystemSettingsModalP
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pt-0">
|
||||
<ProviderModelSelect provider={bgProvider} onProviderChange={(v) => { setBgProvider(v); setBgModel(""); }} model={bgModel} onModelChange={setBgModel} allowEmpty providerLabel={t("bg.provider")} modelLabel={t("bg.model")} providerTip={t("bg.providerTip")} modelTip={t("bg.modelTip")} providerPlaceholder={t("bg.providerPlaceholder")} modelPlaceholder={t("bg.modelPlaceholder")} />
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="providerRequestTimeout" className="text-xs">{t("providers.requestTimeout")}</Label>
|
||||
<Input id="providerRequestTimeout" type="number" min={1} step={1} placeholder="30" value={providerRequestTimeoutSec} onChange={(e) => setProviderRequestTimeoutSec(e.target.value)} className="max-w-[120px] text-base md:text-sm" />
|
||||
<p className="text-xs text-muted-foreground">{t("providers.requestTimeoutHint")}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-700 dark:border-slate-700 dark:bg-slate-950/30 dark:text-slate-300">
|
||||
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0" /><span>{t("bg.info")}</span>
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
"modelPlaceholder": "(default)",
|
||||
"info": "Used for vault enrichment (document summarization), consolidation (session summaries), and dreaming. Leave empty to fall back to the agent default provider."
|
||||
},
|
||||
"providers": {
|
||||
"requestTimeout": "Provider request timeout (seconds)",
|
||||
"requestTimeoutHint": "Timeout for model verification and listing available models (default 30). Increase for slow models. Leave empty to use the default."
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills",
|
||||
"description": "Tenant skill package and slash command settings.",
|
||||
|
||||
@@ -68,6 +68,10 @@
|
||||
"modelPlaceholder": "(기본값)",
|
||||
"info": "볼트 보강 (문서 요약), 통합 (세션 요약), 드리밍에 사용됩니다. 에이전트 기본 공급자로 대체하려면 비워두세요."
|
||||
},
|
||||
"providers": {
|
||||
"requestTimeout": "공급자 요청 시간 초과 (초)",
|
||||
"requestTimeoutHint": "모델 검증 및 사용 가능한 모델 나열에 대한 시간 초과 (기본값 30). 느린 모델의 경우 늘리세요. 기본값을 사용하려면 비워두세요."
|
||||
},
|
||||
"compaction": {
|
||||
"title": "대기 메시지 압축",
|
||||
"description": "컨텍스트 한도 내에 유지하기 위해 긴 대기 메시지 기록을 요약합니다.",
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
"modelPlaceholder": "(mặc định)",
|
||||
"info": "Dùng cho vault enrichment (tóm tắt tài liệu), consolidation (tóm tắt phiên) và dreaming. Để trống sẽ fallback về agent default provider."
|
||||
},
|
||||
"providers": {
|
||||
"requestTimeout": "Thời gian chờ yêu cầu provider (giây)",
|
||||
"requestTimeoutHint": "Thời gian chờ để xác minh model và liệt kê các model khả dụng (mặc định 30). Tăng lên cho các model chậm. Để trống để dùng giá trị mặc định."
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills",
|
||||
"description": "Cài đặt gói skill và slash command theo tenant.",
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
"modelPlaceholder": "(默认)",
|
||||
"info": "用于知识库文档摘要、会话整合和梦境。留空则回退到Agent默认Provider。"
|
||||
},
|
||||
"providers": {
|
||||
"requestTimeout": "Provider请求超时(秒)",
|
||||
"requestTimeoutHint": "用于模型验证和列出可用模型的超时时间(默认30)。慢速模型请调大。留空则使用默认值。"
|
||||
},
|
||||
"skills": {
|
||||
"title": "技能",
|
||||
"description": "租户技能包和斜杠命令设置。",
|
||||
|
||||
Reference in new issue
Block a user