Skip to content

Commit cc55084

Browse files
authored
Merge pull request #503 from phantom5099/main
pref(provider):修正 Provider 生成重试默认语义并放宽退避上限
2 parents 2521416 + d755061 commit cc55084

9 files changed

Lines changed: 157 additions & 8 deletions

File tree

docs/guides/adding-providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ generate_idle_timeout_sec: 300
108108
- `chat_endpoint_path` 为 `/` 表示直连 `base_url`;为空时会按 `chat_api_mode` 自动回填默认子路径(`/chat/completions` 或 `/responses`)。
109109
- 当 `chat_api_mode` 已显式指定时,`chat_endpoint_path` 可使用任意以 `/` 开头的相对路径;未显式指定时,仅支持标准端点推断(`/chat/completions`、`/responses`、`/`)。
110110
- `model_source: manual` 时必须提供 `models`,且会忽略 `discovery_endpoint_path`。
111-
- `generate_max_retries` / `generate_idle_timeout_sec` 用于控制 provider 级生成重试和流空闲超时;未填写或 `<= 0` 时会分别回退到 `5 / 300`。其中 `generate_max_retries` 必须 `<= 20`。
111+
- `generate_max_retries` / `generate_idle_timeout_sec` 用于控制 provider 级生成重试和流空闲超时;`generate_max_retries` 未填写时默认使用 `5`,显式填写 `0` 表示关闭生成重试,`generate_idle_timeout_sec` 未填写或 `<= 0` 时回退到 `300`。其中 `generate_max_retries` 必须 `<= 20`。
112112
- `generate_start_timeout_sec` 已改为根 `config.yaml` 顶层字段,不再允许写入 `provider.yaml`;启动时缺失会自动补写默认值 `90`。
113113

114114
## 测试要求

docs/guides/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ generate_idle_timeout_sec: 300
179179

180180
新增的生成链路控制字段含义如下:
181181

182-
- `generate_max_retries`:额外重试次数,不含首次尝试;`<= 0` 时回退默认值 `5`,且必须 `<= 20`。
182+
- `generate_max_retries`:额外重试次数,不含首次尝试;未填写时默认使用 `5`,显式填写 `0` 表示关闭生成重试,且必须 `<= 20`。
183183
- `generate_start_timeout_sec`:写在 `config.yaml` 顶层,从发请求到收到首个有效流 payload 的最长等待窗口;`<= 0` 时回退默认值 `90`。
184184
- `generate_idle_timeout_sec`:首包后连续没有任何新 payload 的最长空闲窗口;`<= 0` 时回退默认值 `300`。
185185

internal/config/loader_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,55 @@ func TestSaveAndLoadCustomProviderPersistsGenerateControls(t *testing.T) {
13931393
}
13941394
}
13951395

1396+
func TestSaveAndLoadCustomProviderPreservesExplicitZeroGenerateRetries(t *testing.T) {
1397+
t.Parallel()
1398+
1399+
baseDir := t.TempDir()
1400+
const providerName = "zero-retry-provider"
1401+
err := SaveCustomProviderWithModels(baseDir, SaveCustomProviderInput{
1402+
Name: providerName,
1403+
Driver: provider.DriverOpenAICompat,
1404+
BaseURL: "https://llm.example.com/v1",
1405+
APIKeyEnv: "ZERO_RETRY_PROVIDER_API_KEY",
1406+
ModelSource: ModelSourceDiscover,
1407+
DiscoveryEndpointPath: provider.DiscoveryEndpointPathModels,
1408+
GenerateMaxRetries: 0,
1409+
GenerateMaxRetriesSet: true,
1410+
GenerateIdleTimeoutSec: 420,
1411+
})
1412+
if err != nil {
1413+
t.Fatalf("SaveCustomProviderWithModels() error = %v", err)
1414+
}
1415+
1416+
data, err := os.ReadFile(filepath.Join(baseDir, providersDirName, providerName, customProviderConfigName))
1417+
if err != nil {
1418+
t.Fatalf("ReadFile() error = %v", err)
1419+
}
1420+
content := string(data)
1421+
if !strings.Contains(content, "generate_max_retries: 0") {
1422+
t.Fatalf("expected generate_max_retries: 0 to be persisted, got %q", content)
1423+
}
1424+
1425+
cfg, err := loadCustomProvider(filepath.Join(baseDir, providersDirName, providerName))
1426+
if err != nil {
1427+
t.Fatalf("loadCustomProvider() error = %v", err)
1428+
}
1429+
if !cfg.GenerateMaxRetriesSet {
1430+
t.Fatal("expected explicit zero retry setting to remain marked as configured")
1431+
}
1432+
runtimeCfg, err := cfg.Resolve()
1433+
if err != nil {
1434+
t.Fatalf("Resolve() error = %v", err)
1435+
}
1436+
providerRuntimeCfg, err := runtimeCfg.ToRuntimeConfig()
1437+
if err != nil {
1438+
t.Fatalf("ToRuntimeConfig() error = %v", err)
1439+
}
1440+
if providerRuntimeCfg.GenerateMaxRetries != 0 {
1441+
t.Fatalf("expected explicit zero retry setting to disable retries, got %d", providerRuntimeCfg.GenerateMaxRetries)
1442+
}
1443+
}
1444+
13961445
func TestSaveCustomProviderOmitsDefaultGenerateControlsWhenUnset(t *testing.T) {
13971446
t.Parallel()
13981447

@@ -1423,6 +1472,43 @@ func TestSaveCustomProviderOmitsDefaultGenerateControlsWhenUnset(t *testing.T) {
14231472
}
14241473
}
14251474

1475+
func TestLoadCustomProviderUsesDefaultGenerateRetriesWhenUnset(t *testing.T) {
1476+
t.Parallel()
1477+
1478+
baseDir := t.TempDir()
1479+
const providerName = "default-retry-provider"
1480+
err := SaveCustomProviderWithModels(baseDir, SaveCustomProviderInput{
1481+
Name: providerName,
1482+
Driver: provider.DriverOpenAICompat,
1483+
BaseURL: "https://llm.example.com/v1",
1484+
APIKeyEnv: "DEFAULT_RETRY_PROVIDER_API_KEY",
1485+
ModelSource: ModelSourceDiscover,
1486+
DiscoveryEndpointPath: provider.DiscoveryEndpointPathModels,
1487+
})
1488+
if err != nil {
1489+
t.Fatalf("SaveCustomProviderWithModels() error = %v", err)
1490+
}
1491+
1492+
cfg, err := loadCustomProvider(filepath.Join(baseDir, providersDirName, providerName))
1493+
if err != nil {
1494+
t.Fatalf("loadCustomProvider() error = %v", err)
1495+
}
1496+
if cfg.GenerateMaxRetriesSet {
1497+
t.Fatal("expected omitted generate_max_retries to remain unset")
1498+
}
1499+
resolved, err := cfg.Resolve()
1500+
if err != nil {
1501+
t.Fatalf("Resolve() error = %v", err)
1502+
}
1503+
runtimeCfg, err := resolved.ToRuntimeConfig()
1504+
if err != nil {
1505+
t.Fatalf("ToRuntimeConfig() error = %v", err)
1506+
}
1507+
if runtimeCfg.GenerateMaxRetries != provider.DefaultGenerateMaxRetries {
1508+
t.Fatalf("expected omitted generate_max_retries to use default %d, got %d", provider.DefaultGenerateMaxRetries, runtimeCfg.GenerateMaxRetries)
1509+
}
1510+
}
1511+
14261512
func TestLoaderRejectsCustomProviderGenerateStartTimeoutField(t *testing.T) {
14271513
t.Parallel()
14281514

internal/config/provider.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type ProviderConfig struct {
2828
Model string `yaml:"model"`
2929
APIKeyEnv string `yaml:"api_key_env"`
3030
GenerateMaxRetries int `yaml:"generate_max_retries,omitempty"`
31+
GenerateMaxRetriesSet bool `yaml:"-"`
3132
GenerateIdleTimeoutSec int `yaml:"generate_idle_timeout_sec,omitempty"`
3233
ModelSource string `yaml:"-"`
3334
ChatAPIMode string `yaml:"-"`
@@ -169,6 +170,14 @@ func (p ProviderConfig) Resolve() (ResolvedProviderConfig, error) {
169170
}, nil
170171
}
171172

173+
// resolveGenerateMaxRetries 统一解析 provider 级生成重试次数,兼容“未配置使用默认值”和“显式 0 关闭重试”两种语义。
174+
func (p ProviderConfig) resolveGenerateMaxRetries() int {
175+
if p.GenerateMaxRetriesSet || p.GenerateMaxRetries > 0 {
176+
return provider.NormalizeGenerateMaxRetries(p.GenerateMaxRetries)
177+
}
178+
return provider.DefaultGenerateMaxRetries
179+
}
180+
172181
func cloneProviders(providers []ProviderConfig) []ProviderConfig {
173182
if len(providers) == 0 {
174183
return nil
@@ -283,7 +292,7 @@ func (p ResolvedProviderConfig) ToRuntimeConfig() (provider.RuntimeConfig, error
283292
ChatAPIMode: chatAPIMode,
284293
ChatEndpointPath: chatEndpointPath,
285294
DiscoveryEndpointPath: discoveryEndpointPath,
286-
GenerateMaxRetries: provider.NormalizeGenerateMaxRetries(p.GenerateMaxRetries),
295+
GenerateMaxRetries: p.resolveGenerateMaxRetries(),
287296
GenerateStartTimeout: provider.NormalizeGenerateStartTimeout(time.Duration(p.GenerateStartTimeoutSec) * time.Second),
288297
GenerateIdleTimeout: provider.NormalizeGenerateIdleTimeout(time.Duration(p.GenerateIdleTimeoutSec) * time.Second),
289298
}, nil

internal/config/provider_custom_normalize.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ func NormalizeCustomProviderInput(input SaveCustomProviderInput) (SaveCustomProv
2121
ChatEndpointPath: strings.TrimSpace(input.ChatEndpointPath),
2222
APIKeyEnv: strings.TrimSpace(input.APIKeyEnv),
2323
GenerateMaxRetries: normalizeOptionalGenerateInt(input.GenerateMaxRetries),
24+
GenerateMaxRetriesSet: input.GenerateMaxRetriesSet || input.GenerateMaxRetries > 0,
2425
GenerateIdleTimeoutSec: normalizeOptionalGenerateInt(input.GenerateIdleTimeoutSec),
2526
DiscoveryEndpointPath: strings.TrimSpace(input.DiscoveryEndpointPath),
2627
}
@@ -118,6 +119,7 @@ func validateNormalizedCustomProviderInput(input SaveCustomProviderInput) error
118119
BaseURL: input.BaseURL,
119120
APIKeyEnv: input.APIKeyEnv,
120121
GenerateMaxRetries: input.GenerateMaxRetries,
122+
GenerateMaxRetriesSet: input.GenerateMaxRetriesSet,
121123
GenerateIdleTimeoutSec: input.GenerateIdleTimeoutSec,
122124
ModelSource: input.ModelSource,
123125
ChatAPIMode: input.ChatAPIMode,

internal/config/provider_loader.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type customProviderFile struct {
2424
Name string `yaml:"name"`
2525
Driver string `yaml:"driver"`
2626
APIKeyEnv string `yaml:"api_key_env"`
27-
GenerateMaxRetries int `yaml:"generate_max_retries,omitempty"`
27+
GenerateMaxRetries *int `yaml:"generate_max_retries,omitempty"`
2828
GenerateIdleTimeoutSec int `yaml:"generate_idle_timeout_sec,omitempty"`
2929
ModelSource string `yaml:"model_source,omitempty"`
3030
ChatAPIMode string `yaml:"chat_api_mode,omitempty"`
@@ -115,7 +115,8 @@ func loadCustomProvider(providerDir string) (ProviderConfig, error) {
115115
Driver: strings.TrimSpace(file.Driver),
116116
BaseURL: strings.TrimSpace(file.BaseURL),
117117
APIKeyEnv: strings.TrimSpace(file.APIKeyEnv),
118-
GenerateMaxRetries: file.GenerateMaxRetries,
118+
GenerateMaxRetries: optionalIntValue(file.GenerateMaxRetries),
119+
GenerateMaxRetriesSet: file.GenerateMaxRetries != nil,
119120
GenerateIdleTimeoutSec: file.GenerateIdleTimeoutSec,
120121
ModelSource: strings.TrimSpace(file.ModelSource),
121122
ChatAPIMode: strings.TrimSpace(file.ChatAPIMode),
@@ -133,6 +134,7 @@ func loadCustomProvider(providerDir string) (ProviderConfig, error) {
133134
BaseURL: normalizedInput.BaseURL,
134135
APIKeyEnv: normalizedInput.APIKeyEnv,
135136
GenerateMaxRetries: normalizedInput.GenerateMaxRetries,
137+
GenerateMaxRetriesSet: normalizedInput.GenerateMaxRetriesSet,
136138
GenerateIdleTimeoutSec: normalizedInput.GenerateIdleTimeoutSec,
137139
ModelSource: normalizedInput.ModelSource,
138140
ChatAPIMode: normalizedInput.ChatAPIMode,
@@ -204,6 +206,7 @@ type SaveCustomProviderInput struct {
204206
ChatEndpointPath string
205207
APIKeyEnv string
206208
GenerateMaxRetries int
209+
GenerateMaxRetriesSet bool
207210
GenerateIdleTimeoutSec int
208211
DiscoveryEndpointPath string
209212
ModelSource string
@@ -226,7 +229,7 @@ func SaveCustomProviderWithModels(baseDir string, input SaveCustomProviderInput)
226229
Name: normalizedInput.Name,
227230
Driver: normalizedInput.Driver,
228231
APIKeyEnv: normalizedInput.APIKeyEnv,
229-
GenerateMaxRetries: normalizedInput.GenerateMaxRetries,
232+
GenerateMaxRetries: optionalIntPointer(normalizedInput.GenerateMaxRetries, normalizedInput.GenerateMaxRetriesSet || normalizedInput.GenerateMaxRetries > 0),
230233
GenerateIdleTimeoutSec: normalizedInput.GenerateIdleTimeoutSec,
231234
ModelSource: normalizedInput.ModelSource,
232235
ChatAPIMode: normalizedInput.ChatAPIMode,
@@ -313,3 +316,20 @@ func validateCustomProviderName(name string) error {
313316
}
314317
return nil
315318
}
319+
320+
// optionalIntValue 统一读取可选整数字段,避免缺省值和显式零值在解析阶段丢失原始语义。
321+
func optionalIntValue(value *int) int {
322+
if value == nil {
323+
return 0
324+
}
325+
return *value
326+
}
327+
328+
// optionalIntPointer 根据是否显式配置决定是否输出 YAML 字段,保留“未配置”和“显式 0”两种语义差异。
329+
func optionalIntPointer(value int, configured bool) *int {
330+
if !configured {
331+
return nil
332+
}
333+
out := value
334+
return &out
335+
}

internal/config/provider_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,7 @@ func TestResolvedProviderConfigToRuntimeConfig(t *testing.T) {
776776
ChatAPIMode: "",
777777
ChatEndpointPath: "",
778778
DiscoveryEndpointPath: providerpkg.DiscoveryEndpointPathModels,
779-
GenerateMaxRetries: 0,
779+
GenerateMaxRetries: providerpkg.DefaultGenerateMaxRetries,
780780
GenerateStartTimeout: providerpkg.DefaultGenerateStartTimeout,
781781
GenerateIdleTimeout: providerpkg.DefaultGenerateIdleTimeout,
782782
}
@@ -802,6 +802,30 @@ func TestResolvedProviderConfigToRuntimeConfig(t *testing.T) {
802802
}
803803
}
804804

805+
func TestResolvedProviderConfigToRuntimeConfigPreservesExplicitZeroGenerateRetries(t *testing.T) {
806+
t.Parallel()
807+
808+
resolved := ResolvedProviderConfig{
809+
ProviderConfig: ProviderConfig{
810+
Name: "company-gateway",
811+
Driver: "openaicompat",
812+
BaseURL: "https://llm.example.com/v1",
813+
Model: "server-default",
814+
APIKeyEnv: "COMPANY_GATEWAY_KEY",
815+
GenerateMaxRetries: 0,
816+
GenerateMaxRetriesSet: true,
817+
},
818+
}
819+
820+
got, err := resolved.ToRuntimeConfig()
821+
if err != nil {
822+
t.Fatalf("ToRuntimeConfig() error = %v", err)
823+
}
824+
if got.GenerateMaxRetries != 0 {
825+
t.Fatalf("expected explicit GenerateMaxRetries=0 to disable retries, got %d", got.GenerateMaxRetries)
826+
}
827+
}
828+
805829
func TestResolvedProviderConfigToRuntimeConfigMapsGenerateControls(t *testing.T) {
806830
t.Parallel()
807831

internal/provider/constants.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const (
2323
// DefaultGenerateRetryBaseWait 定义生成链路重试退避的基础等待时长。
2424
DefaultGenerateRetryBaseWait = 1 * time.Second
2525
// DefaultGenerateRetryMaxWait 定义生成链路重试退避的最大等待时长。
26-
DefaultGenerateRetryMaxWait = 5 * time.Second
26+
DefaultGenerateRetryMaxWait = 7 * time.Second
2727
// DefaultSDKRequestTimeout 定义非生成链路访问外部模型 SDK 的统一保底超时。
2828
DefaultSDKRequestTimeout = 10 * time.Minute
2929
)

internal/provider/constants_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,11 @@ func TestNormalizeGenerateIdleTimeout(t *testing.T) {
5959
t.Fatalf("NormalizeGenerateIdleTimeout(4s) = %s, want %s", got, want)
6060
}
6161
}
62+
63+
func TestDefaultGenerateRetryMaxWait(t *testing.T) {
64+
t.Parallel()
65+
66+
if DefaultGenerateRetryMaxWait != 7*time.Second {
67+
t.Fatalf("DefaultGenerateRetryMaxWait = %s, want 7s", DefaultGenerateRetryMaxWait)
68+
}
69+
}

0 commit comments

Comments
 (0)